<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Le blogue de développement de Curling IO</title>
        <link>https://curling.io/fr/blog</link>
        <description>Le développement et l’amélioration de Curling IO</description>
        <lastBuildDate>Mon, 12 Oct 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>fr-CA</language>
        <copyright>Droits d’auteur © 2026 Curling IO</copyright>
        <item>
            <title><![CDATA[Aider les joueurs autonomes à trouver une équipe]]></title>
            <link>https://curling.io/fr/blog/free-agents</link>
            <guid>https://curling.io/fr/blog/free-agents</guid>
            <pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Permettez aux personnes inscrites d’indiquer qu’elles cherchent une équipe, puis communiquez avec elles et placez-les dans un alignement.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Utilisez les joueurs autonomes pour trouver des curleurs qui souhaitent rejoindre une équipe dans votre ligue ou votre bonspiel. Cette fonction ajoute une option de consentement à l’inscription et donne à vos gestionnaires une liste de personnes à contacter.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="registration-fees-still-apply">Les frais d’inscription s’appliquent toujours<a href="https://curling.io/fr/blog/free-agents#registration-fees-still-apply" class="hash-link" aria-label="Lien direct vers Les frais d’inscription s’appliquent toujours" title="Lien direct vers Les frais d’inscription s’appliquent toujours" translate="no">​</a></h2>
<p>Les joueurs autonomes s’inscrivent à l’événement par votre parcours de commande habituel. Les prix, les rabais et les options de paiement habituels de l’événement s’appliquent. Choisir <strong>M’inscrire comme joueur autonome</strong> n’annule pas les frais d’inscription et ne reporte pas le paiement jusqu’à ce que la personne trouve une équipe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="let-curlers-opt-in">Permettre aux curleurs de s’inscrire à la liste<a href="https://curling.io/fr/blog/free-agents#let-curlers-opt-in" class="hash-link" aria-label="Lien direct vers Permettre aux curleurs de s’inscrire à la liste" title="Lien direct vers Permettre aux curleurs de s’inscrire à la liste" translate="no">​</a></h2>
<ol>
<li class="">Allez à <strong>Produits</strong> et ouvrez votre ligue ou votre bonspiel.</li>
<li class="">Sélectionnez <strong>Information</strong>.</li>
<li class="">Cochez <strong>Permettre les joueurs autonomes</strong>, puis sélectionnez <strong>Enregistrer</strong>.</li>
</ol>
<p>L’événement doit exiger un profil de participant. Si l’option n’apparaît pas, vérifiez la configuration de l’événement sous <strong>Information</strong>.</p>
<p><img decoding="async" loading="lazy" alt="L’option permettant les joueurs autonomes activée dans la page d’information de l’événement." src="https://curling.io/fr/assets/images/enable-free-agents-en-fd01ed7eec58b715493361d8bc91dba0.png" width="2880" height="1120" class="img_amcT"></p>
<p>Les curleurs peuvent cocher <strong>M’inscrire comme joueur autonome</strong> pendant l’inscription. Ce choix partage le courriel et le téléphone disponibles dans le profil de participant avec les autres personnes inscrites admissibles du même événement. Le profil doit contenir un courriel ou un numéro de téléphone pour permettre de joindre la personne.</p>
<p>Les coordonnées proviennent du profil de participant, qui peut utiliser un courriel différent de celui du compte de connexion. Encouragez les curleurs à vérifier ces renseignements avant de s’inscrire.</p>
<p>Lors de la commande, choisir cette option efface les réponses de nom d’équipe et de nom du capitaine. Les autres questions dépendent toujours de votre formulaire d’inscription. Les curleurs doivent terminer la soumission de leur inscription avant d’apparaître dans la liste. Un panier inachevé n’ajoute personne à la liste.</p>
<p><img decoding="async" loading="lazy" alt="Un membre choisit de s’inscrire comme joueur autonome pendant l’inscription." src="https://curling.io/fr/assets/images/free-agent-registration-en-46100eb1a934b410f8e01ea42df87a47.png" width="2880" height="1240" class="img_amcT"></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="contact-them-and-build-the-lineup">Communiquer avec eux et composer l’alignement<a href="https://curling.io/fr/blog/free-agents#contact-them-and-build-the-lineup" class="hash-link" aria-label="Lien direct vers Communiquer avec eux et composer l’alignement" title="Lien direct vers Communiquer avec eux et composer l’alignement" translate="no">​</a></h2>
<p>Ouvrez <strong>Joueurs autonomes</strong> pour l’événement afin de consulter le nom de chaque participant ainsi que le courriel et le téléphone disponibles dans son profil.</p>
<ol>
<li class="">Communiquez avec le curleur pour discuter de l’équipe et de la position.</li>
<li class="">Ouvrez <strong>Équipes</strong> pour le même événement.</li>
<li class="">Créez une équipe ou modifiez celle qu’il rejoindra.</li>
<li class="">Sélectionnez le participant dans l’alignement de l’équipe, puis sélectionnez <strong>Enregistrer</strong>.</li>
</ol>
<p>Une fois placé dans une équipe de cet événement, le participant disparaît automatiquement de la liste des joueurs autonomes. Vous n’avez pas à le retirer manuellement. Son inscription et ses paiements restent inchangés. S’il quitte l’alignement de l’équipe alors que son consentement est toujours actif, il peut réapparaître dans la liste. Curling IO ne place pas automatiquement les joueurs autonomes dans des équipes.</p>
<p><img decoding="async" loading="lazy" alt="La liste de coordonnées des joueurs autonomes d’un événement." src="https://curling.io/fr/assets/images/free-agents-en-676a8f270d0dc998925b7582373c464d.png" width="2880" height="860" class="img_amcT"></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="who-can-see-the-contact-details">Qui peut voir les coordonnées?<a href="https://curling.io/fr/blog/free-agents#who-can-see-the-contact-details" class="hash-link" aria-label="Lien direct vers Qui peut voir les coordonnées?" title="Lien direct vers Qui peut voir les coordonnées?" translate="no">​</a></h2>
<p>Sur le site public du club, l’onglet <strong>Joueurs autonomes</strong> de l’événement est accessible aux comptes connectés qui possèdent une inscription soumise ou payée à cet événement. L’inscription ne doit pas être annulée. Le répertoire public devient indisponible après la fin de l’événement ou si l’événement ou l’option des joueurs autonomes est désactivé.</p>
<p>Les visiteurs anonymes et les comptes sans inscription admissible ne peuvent pas consulter le répertoire. Les gestionnaires du club peuvent le consulter dans la page d’administration de l’événement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="remove-someone-from-the-list">Retirer une personne de la liste<a href="https://curling.io/fr/blog/free-agents#remove-someone-from-the-list" class="hash-link" aria-label="Lien direct vers Retirer une personne de la liste" title="Lien direct vers Retirer une personne de la liste" translate="no">​</a></h2>
<p>Si un curleur ne souhaite plus être contacté, sélectionnez <strong>Retirer des joueurs autonomes</strong> à côté de son nom, puis <strong>Confirmer</strong>. Cela efface son consentement actuel à figurer parmi les joueurs autonomes de cet événement et le retire de la liste de coordonnées. Cette action n’annule pas son inscription à l’événement, ne le retire pas d’une équipe, ne modifie pas ses paiements et ne produit aucun remboursement. Son profil de participant est aussi conservé.</p>
<p>Lorsqu’une personne trouve une équipe, enregistrez plutôt l’alignement de l’équipe. Pour annuler son inscription ou rembourser un paiement, utilisez les actions distinctes de gestion des commandes et des remboursements.</p>]]></content:encoded>
            <category>Curling</category>
            <category>Fonctions</category>
            <category>v3</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Plans de paiement et versements]]></title>
            <link>https://curling.io/fr/blog/payment-plans-and-installments</link>
            <guid>https://curling.io/fr/blog/payment-plans-and-installments</guid>
            <pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Offrez des versements prévus sur les produits de votre choix. Les membres voient chaque paiement et sa date d’échéance avant de finaliser leur commande, et le personnel peut suivre les versements refusés ou en retard dans Curling IO.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Un membre veut répartir ses frais de saison sur quelques mois. Recueillir ces paiements par virement Interac et les suivre dans un tableur peut être difficile.</p>
<p>Les plans de paiement permettent de répartir certains achats en versements prévus. Les membres choisissent un plan à la finalisation de la commande, SportsPay programme le prélèvement automatique des versements suivants, et chaque paiement reste lié à la commande du membre dans Curling IO.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="offer-a-payment-plan-for-a-registration">Offrir un plan de paiement pour une inscription<a href="https://curling.io/fr/blog/payment-plans-and-installments#offer-a-payment-plan-for-a-registration" class="hash-link" aria-label="Lien direct vers Offrir un plan de paiement pour une inscription" title="Lien direct vers Offrir un plan de paiement pour une inscription" translate="no">​</a></h2>
<p>Vous choisissez quels produits offrent un plan. Les membres peuvent toujours payer en totalité. Un plan a une fin précise : une fois tous les versements payés, les prélèvements cessent.</p>
<p>Commencez dans <strong>Paramètres &gt; Paiements &gt; Politiques des plans de paiement</strong>. Choisissez les politiques du club pour l’annulation des versements restants et le remboursement des paiements déjà perçus. Ces deux choix sont obligatoires avant d’activer le plan d’un produit. Les politiques apparaissent dans l’autorisation du client à la finalisation de la commande.</p>
<p><img decoding="async" loading="lazy" alt="Les Politiques des plans de paiement dans Paramètres &amp;gt; Paiements, avec Paiements en surbrillance dans la navigation." src="https://curling.io/fr/assets/images/plan-policies-e023f3a2e0f97dc6efde10d7761662f5.png" width="2880" height="2000" class="img_amcT"></p>
<p>Sur la page de modification du produit, activez <strong>Offrir un plan de paiement</strong> sous <strong>Tarification</strong>. Choisissez un nombre de paiements ou un montant par paiement, puis définissez le nombre de mois entre les paiements. L’aperçu montre un exemple d’échéancier pour quelqu’un qui achète aujourd’hui.</p>
<p>Par exemple, vous pourriez offrir trois paiements mensuels. Si vous fixez plutôt un montant par paiement, le dernier versement peut être plus petit que les autres.</p>
<p><img decoding="async" loading="lazy" alt="La tarification du produit avec Offrir un plan de paiement activé, trois paiements et un mois entre les paiements." src="https://curling.io/fr/assets/images/product-plan-a06f8dd17f989adcbc4d625b69d265da.png" width="2880" height="1760" class="img_amcT"></p>
<p>L’échéancier final utilise le prix du produit après les rabais et les taxes. Les autres articles du panier et les frais applicables à la commande sont dus avec le premier versement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="show-members-what-theyll-pay">Montrer aux membres ce qu’ils paieront<a href="https://curling.io/fr/blog/payment-plans-and-installments#show-members-what-theyll-pay" class="hash-link" aria-label="Lien direct vers Montrer aux membres ce qu’ils paieront" title="Lien direct vers Montrer aux membres ce qu’ils paieront" translate="no">​</a></h2>
<p>Le membre choisit le plan dans son panier. Le parcours de commande montre le montant et la date d’échéance de chaque versement, le montant dû maintenant et les politiques d’annulation et de remboursement du club. Finaliser la commande autorise les prélèvements prévus.</p>
<p><img decoding="async" loading="lazy" alt="Une commande de démonstration montre un achat de 30 $ réparti en trois paiements de 10 $, les dates et le texte d’autorisation du club." src="https://curling.io/fr/assets/images/checkout-schedule-8c43aa8ba151afeaa706a7464b0d1f9f.png" width="2880" height="2000" class="img_amcT"></p>
<p>Après la réussite du premier paiement, Curling IO configure les versements restants avec SportsPay pour leur prélèvement automatique. Curling IO envoie des rappels par courriel avant les échéances et des avis lorsqu’un versement réussit ou est refusé. Les numéros de carte restent chez le processeur de paiement; Curling IO ne les conserve pas.</p>
<p>Si le parcours de commande affiche <strong>Votre commande est en cours de traitement</strong>, Curling IO confirme encore le paiement. Le membre peut quitter la page et revenir à la commande plus tard. Il devrait attendre le résultat avant d’essayer un autre paiement.</p>
<p>Si un paiement est refusé, les versements suivants sont suspendus. Le membre peut ouvrir <strong>Compte &gt; Plans de paiement</strong> et mettre à jour son mode de paiement. La mise à jour de la carte entraîne aussi une nouvelle tentative du versement refusé le plus ancien. Si plusieurs paiements sont en retard, le rattrapage peut entraîner plus d’un prélèvement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="follow-up-on-plans-that-need-attention">Suivre les plans qui demandent votre attention<a href="https://curling.io/fr/blog/payment-plans-and-installments#follow-up-on-plans-that-need-attention" class="hash-link" aria-label="Lien direct vers Suivre les plans qui demandent votre attention" title="Lien direct vers Suivre les plans qui demandent votre attention" translate="no">​</a></h2>
<p>Ouvrez <strong>Inscriptions &gt; Plans de paiement</strong> pour voir les plans des commandes du club. Filtrez selon des états comme <strong>Échec</strong>, <strong>À vérifier</strong> ou <strong>En retard</strong> pour trouver ceux qui exigent un suivi. Ouvrez la commande d’un plan pour examiner ses versements et réessayer un paiement refusé.</p>
<p>Chaque ligne montre le numéro du plan, le produit, le compte, la prochaine date de paiement, les montants payés et restants et l’état. Si SportsPay signale une erreur, un message sous le plan explique le problème.</p>
<p><img decoding="async" loading="lazy" alt="La file des Plans de paiement montre les numéros de plan, les produits, la prochaine date de paiement, les montants payés et restants et l’état de chaque plan, avec la navigation d’administration repliée." src="https://curling.io/fr/assets/images/plan-queue-ad1ecfa424e616047ad0e7f3a513e140.png" width="2880" height="2000" class="img_amcT"></p>
<p>Le personnel peut aussi annuler les versements restants. Cela réduit le montant impayé, mais ne rembourse pas les paiements déjà perçus et n’annule pas l’inscription. Lisez le résultat de l’annulation avant de présumer que les prochains prélèvements ont cessé. Si la suppression dans SportsPay n’est pas confirmée, un prélèvement prévu peut encore avoir lieu et exige un suivi.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="where-payment-plans-are-available">Où les plans de paiement sont offerts<a href="https://curling.io/fr/blog/payment-plans-and-installments#where-payment-plans-are-available" class="hash-link" aria-label="Lien direct vers Où les plans de paiement sont offerts" title="Lien direct vers Où les plans de paiement sont offerts" translate="no">​</a></h2>
<p>Les plans de paiement utilisent actuellement <strong>SportsPay</strong>. Les plans Stripe ne sont pas encore offerts. L’activation et la configuration des paiements en ligne donnent aussi accès à Premium. Un club qui utilise SportsPay n’a donc pas besoin d’un achat Premium distinct pour offrir ces plans.</p>]]></content:encoded>
            <category>v3</category>
            <category>Gestion de club</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Mettre à jour le site de votre club avec Studio]]></title>
            <link>https://curling.io/fr/blog/website-studio</link>
            <guid>https://curling.io/fr/blog/website-studio</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Demandez à l’IA de vous aider à modifier le site de votre club, vérifiez l’aperçu et publiez quand vous êtes prêt. Studio conserve un historique pour restaurer des conceptions antérieures.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Si votre club utilise un système de gestion de contenu (CMS), cette situation vous sera peut-être familière. Un membre du personnel ou un bénévole crée le site, mais lorsqu’il part, personne d’autre ne sait le mettre à jour. Les modifications sont reportées, et les vieilles informations restent en ligne.</p>
<p>Nous avons construit Studio pour permettre au personnel du club de garder son site à jour aussi facilement que possible. Votre site se trouve dans Curling IO, avec les programmes, les événements et les inscriptions du club. Vous n’avez pas à gérer un site tiers compliqué, comme WordPress, ni à payer un autre fournisseur uniquement pour l’hébergement.</p>
<!-- -->
<div class="text--center videoWrapper"><iframe width="100%" src="https://www.youtube.com/embed/Bpjhtp0UiXg" title="Modifier le site de votre club avec l’IA | Studio de site web Curling IO" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div>
<p>Les inscriptions ouvrent, un nouveau programme a besoin d’un lien bien visible et le message d’accueil pourrait être réécrit. Studio vous donne un aperçu du site et un assistant IA pour vous aider à faire ces changements.</p>
<p>Décrivez ce que vous voulez changer et vérifiez le résultat dans l’aperçu avant de publier. Si vous préférez une conception antérieure, vous pouvez facilement la restaurer.</p>
<p><img decoding="async" loading="lazy" alt="Studio montre l’aperçu de la page d’accueil du club de démonstration et la barre d’outils de modification." src="https://curling.io/fr/assets/images/preview-737afb7585bc04e69c1fee8fd04639c6.png" width="2880" height="1800" class="img_amcT"></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="curling-ios-built-in-ai-assistant">L’assistant IA intégré de Curling IO<a href="https://curling.io/fr/blog/website-studio#curling-ios-built-in-ai-assistant" class="hash-link" aria-label="Lien direct vers L’assistant IA intégré de Curling IO" title="Lien direct vers L’assistant IA intégré de Curling IO" translate="no">​</a></h2>
<p>Le moyen le plus simple de faire un changement est de le demander à l’assistant intégré de Curling IO. Ouvrez <strong>Site web &gt; Studio</strong>, sélectionnez <strong>Modifier</strong>, puis le bouton avec des étincelles nommé <strong>Demander de l'aide à l'IA pour cette tâche</strong>. Assurez-vous que l’onglet <strong>Assistant</strong> est sélectionné. Vous pouvez travailler à côté de l’aperçu, sans modifier vous-même les paramètres du site.</p>
<p>Dites-lui ce que vous voulez changer. Par exemple :</p>
<blockquote>
<p>Rends notre page d’accueil plus accueillante pour les nouveaux curleurs. Rédige une courte introduction et ajoute un bouton Renseignez-vous sur l’initiation au curling qui mène à notre page de contact.</p>
</blockquote>
<p>L’assistant fait le changement dans l’aperçu pour que vous voyiez immédiatement le résultat. Sélectionnez <strong>Accepter</strong> pour l’enregistrer ou <strong>Refuser</strong> pour revenir au brouillon précédent. Refuser retire le changement abandonné sans l’ajouter à l’historique du site. Votre site public reste inchangé tant que vous ne sélectionnez pas <strong>Publier</strong>.</p>
<p>Précisez le résultat souhaité. Dites quelle page ou section modifier, fournissez les faits nécessaires et nommez ce que vous voulez conserver. Si un programme commence à une date précise, indiquez-la. Lisez le texte proposé avant de l’accepter, surtout les noms, les dates et les affirmations sur votre club.</p>
<p>Vous pouvez aussi donner des images à l’assistant. Sélectionnez <strong>Ajouter une image</strong>, puis choisissez son usage. <strong>Site Web</strong> utilise l’image sur votre site. <strong>Référence</strong> montre à l’assistant une image à consulter. Une image de référence ne devient pas une image du site simplement parce que vous l’avez jointe.</p>
<p><img decoding="async" loading="lazy" alt="L’assistant intégré de Curling IO montre un aperçu modifié avec les commandes Accepter et Refuser." src="https://curling.io/fr/assets/images/built-in-assistant-01f650c5fca442f0438d74649ec6c7f2.png" width="2560" height="1440" class="img_amcT"></p>
<p>Vous pouvez demander un autre ajustement dans la même conversation, par exemple raccourcir un titre qui s’intègre mal. Pour une autre tâche, sélectionnez <strong>Nouveau clavardage</strong>. Cela efface la conversation tout en gardant les modifications du site déjà acceptées.</p>
<p>Vous pourriez ensuite ajouter une image de bannière à une autre page, retirer un lien du menu du site ou changer l’ordre des liens. Prévisualisez et acceptez chaque changement, puis publiez une seule fois la mise à jour terminée. Donnez à l’assistant les faits de votre club et la destination de tout nouveau lien, puis vérifiez sa proposition.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="or-bring-your-own-ai-agent">Ou utilisez votre propre agent IA<a href="https://curling.io/fr/blog/website-studio#or-bring-your-own-ai-agent" class="hash-link" aria-label="Lien direct vers Ou utilisez votre propre agent IA" title="Lien direct vers Ou utilisez votre propre agent IA" translate="no">​</a></h2>
<p>Si vous préférez travailler avec votre propre agent, sélectionnez l’onglet <strong>Mon agent</strong> dans le panneau de l’assistant. Il explique comment commencer et fournit des instructions à copier. Commencez la conversation dans votre propre assistant IA, puis donnez-lui accès à la page par une connexion de navigateur prise en charge.</p>
<p>Votre agent doit accéder à la page d’administration où votre session est ouverte, par une connexion de navigateur prise en charge. Coller l’adresse du club dans un clavardage ordinaire ne lui donne pas cet accès.</p>
<p><img decoding="async" loading="lazy" alt="Le panneau Mon agent avec les instructions de connexion et la commande Copier les instructions." src="https://curling.io/fr/assets/images/your-own-agent-f0da0afab7408a1f9024ac51ea5562de.png" width="2560" height="1440" class="img_amcT"></p>
<p>Sur une page qui offre des actions aux agents, ouvrez la page dans votre agent de navigateur compatible, choisissez <strong>Copier les instructions</strong> et donnez-lui ces instructions avec le changement souhaité. Les actions disponibles dépendent de la page et de vos permissions. Vous choisissez et téléversez encore vous-même les fichiers et répondez aux confirmations requises.</p>
<p>Nous rendons la prise en charge des agents externes accessible dans toute l’administration. La même approche vous permettra de demander de l’aide sur la page où vous travaillez. Les actions possibles dépendent de la page et de vos permissions; le panneau de connexion montre ce qui est disponible. Studio met des actions à la disposition des agents externes, comme ouvrir l’historique des modifications ou proposer un changement à vérifier. L’exemple de modification présenté ici utilise l’assistant intégré.</p>
<p>Pour revenir à l’assistant intégré, sélectionnez l’onglet <strong>Assistant</strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="choose-what-belongs-on-your-homepage">Choisir le contenu de votre page d’accueil<a href="https://curling.io/fr/blog/website-studio#choose-what-belongs-on-your-homepage" class="hash-link" aria-label="Lien direct vers Choisir le contenu de votre page d’accueil" title="Lien direct vers Choisir le contenu de votre page d’accueil" translate="no">​</a></h2>
<p>Studio vous permet d’organiser les sections de la page d’accueil selon ce que les visiteurs doivent trouver. Vous pouvez demander à l’assistant de changer le texte, de réorganiser les sections ou d’ajouter des composants pris en charge.</p>
<table><thead><tr><th>Section</th><th>Ce que vous pouvez modifier ou montrer</th></tr></thead><tbody><tr><td>Section d’accueil</td><td>Le titre d’ouverture, l’introduction, l’image et jusqu’à quatre boutons.</td></tr><tr><td>En bref</td><td>Jusqu’à quatre courts points sur votre club.</td></tr><tr><td>Texte</td><td>Des titres et du texte pour les introductions ou d’autres renseignements.</td></tr><tr><td>Liens rapides</td><td>Jusqu’à huit liens vers des destinations utiles.</td></tr><tr><td>Titre et bouton</td><td>Un titre, une explication et un bouton pour une prochaine étape, comme contacter le club.</td></tr><tr><td>Témoignages</td><td>Jusqu’à trois citations avec leurs attributions, fournies par votre club.</td></tr><tr><td>Types de produits</td><td>Des liens et des résumés tirés de vos types de produits activés.</td></tr><tr><td>Événements à venir et articles récents</td><td>Le contenu public actuel tiré des fiches du club.</td></tr><tr><td>Commanditaires</td><td>Les commanditaires admissibles des fiches de votre club.</td></tr><tr><td>Locations et parties en cours</td><td>Les renseignements courants du club dans les composants de site pris en charge.</td></tr></tbody></table>
<p>Les sections dynamiques vous évitent de maintenir une seconde copie des renseignements du club sur la page d’accueil. Elles utilisent les enregistrements publics admissibles de Curling IO. Changer la mise en page du site ne modifie pas la fiche d’événement, de produit ou de commanditaire sous-jacente.</p>
<p>Vous pouvez aussi choisir ce qui s’ouvre comme page d’accueil : la page d’accueil intégrée, une page publiée ou le premier type de produit automatique.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="add-widgets-to-your-website">Ajouter des widgets à votre site<a href="https://curling.io/fr/blog/website-studio#add-widgets-to-your-website" class="hash-link" aria-label="Lien direct vers Ajouter des widgets à votre site" title="Lien direct vers Ajouter des widgets à votre site" translate="no">​</a></h2>
<p>Les widgets vous permettent d’ajouter du contenu et des services utiles à votre site. Vous pouvez montrer les publications sociales du club, ajouter une inscription à l’infolettre ou aider les visiteurs à trouver l’aréna sur une carte.</p>
<table><thead><tr><th>Widget</th><th>Ce que vous pouvez ajouter</th></tr></thead><tbody><tr><td>Instagram</td><td>Un profil, une publication individuelle ou un Reel.</td></tr><tr><td>Facebook</td><td>Le fil d’une page Facebook.</td></tr><tr><td>X</td><td>Le fil d’un profil.</td></tr><tr><td>TikTok</td><td>Un profil de créateur ou une vidéo individuelle.</td></tr><tr><td>YouTube</td><td>Une vidéo ou une liste de lecture.</td></tr><tr><td>Calendriers</td><td>Un calendrier Google Agenda ou Timely.</td></tr><tr><td>Cartes</td><td>Une carte Google intégrée.</td></tr><tr><td>Documents</td><td>Des documents Google et OneDrive intégrés pris en charge.</td></tr><tr><td>Météo</td><td>Un widget météo d’Environnement Canada.</td></tr><tr><td>Inscription à l’infolettre</td><td>Un formulaire d’inscription Mailchimp ou EmailOctopus.</td></tr><tr><td>Cartes-cadeaux</td><td>Un parcours d’achat Gift Up.</td></tr><tr><td>Diaporama de photos</td><td>Des photos du club avec légendes et liens facultatifs.</td></tr><tr><td>Onglets et sections dépliables</td><td>Des renseignements groupés par onglets ou des sections que les visiteurs peuvent ouvrir pour en lire davantage.</td></tr><tr><td>Renseignements du club</td><td>La disponibilité des locations, les parties en cours, les commanditaires, les coordonnées et les avis publics.</td></tr></tbody></table>
<p>Pour les profils sociaux, Studio peut suggérer les comptes déjà enregistrés dans les paramètres du club. Vous pouvez changer le compte à l’ajout du widget. Les publications, les vidéos et les listes de lecture individuelles utilisent le lien précis fourni.</p>
<p>Par exemple, demandez à l’assistant : « Ajoute notre profil Instagram sous le message d’accueil » ou « Ajoute une inscription à l’infolettre près du bas de la page d’accueil ». Fournissez le compte, le lien ou les détails d’inscription nécessaires.</p>
<p>Vous avez besoin d’un widget absent de la liste? Vous pouvez demander de nouveaux widgets dans la section <strong>Feuille de route</strong>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="customize-the-rest-of-the-site">Personnaliser le reste du site<a href="https://curling.io/fr/blog/website-studio#customize-the-rest-of-the-site" class="hash-link" aria-label="Lien direct vers Personnaliser le reste du site" title="Lien direct vers Personnaliser le reste du site" translate="no">​</a></h2>
<p>Studio contrôle aussi le thème et la navigation du site. Vous pouvez organiser les liens pour que les visiteurs trouvent les ligues, les pages, le calendrier et les autres contenus publics. Les pages prises en charge ont de l’espace pour des composants supplémentaires au-dessus et au-dessous de leur contenu principal, dont des bannières et du texte d’introduction.</p>
<p><img decoding="async" loading="lazy" alt="Une image de bannière ajoutée à la page de contact dans l’aperçu privé de Studio." src="https://curling.io/fr/assets/images/banner-preview-9bb6eba9684b4ea95ed66fdd8f0066aa.png" width="2560" height="1440" class="img_amcT"></p>
<p>La personnalisation a ses limites. Studio utilise des composants et des paramètres pris en charge. Il ne laisse pas l’assistant écrire des scripts arbitraires ou remplacer les parcours d’inscription et de paiement. Demandez un changement concret, comme ajouter une introduction au-dessus d’une liste de programmes, plutôt que de présumer qu’il peut construire toutes les pages imaginables.</p>
<p>Si votre site utilise l’anglais et le français, vérifiez les deux langues avant de publier. Un changement qui se lit bien dans une langue doit aussi être vérifié dans l’autre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="preview-changes-without-publishing-them">Prévisualiser les changements sans les publier<a href="https://curling.io/fr/blog/website-studio#preview-changes-without-publishing-them" class="hash-link" aria-label="Lien direct vers Prévisualiser les changements sans les publier" title="Lien direct vers Prévisualiser les changements sans les publier" translate="no">​</a></h2>
<p>Vous pouvez essayer un autre titre, réorganiser les sections ou examiner plusieurs suggestions de l’IA sans changer ce que voient les visiteurs. Studio permet de prévisualiser vos changements enregistrés pendant que le site public continue de montrer sa dernière version publiée.</p>
<p>Sélectionnez <strong>Enregistrer</strong> pour créer un <strong>Point de contrôle</strong>, un brouillon enregistré auquel vous pouvez revenir. Accepter un changement de l’assistant crée aussi un point de contrôle. Vérifiez le résultat dans l’aperçu de Studio, faites un autre ajustement et enregistrez encore au besoin. Vous n’avez pas à publier chaque changement au fur et à mesure.</p>
<p>Par exemple, vous pourriez réécrire le message d’accueil et ajouter un lien d’inscription, puis vérifier leur apparence ensemble. Si le texte exige une autre retouche, continuez de modifier le brouillon. Vos visiteurs voient toujours le site existant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="publish-when-youre-ready">Publier quand vous êtes prêt<a href="https://curling.io/fr/blog/website-studio#publish-when-youre-ready" class="hash-link" aria-label="Lien direct vers Publier quand vous êtes prêt" title="Lien direct vers Publier quand vous êtes prêt" translate="no">​</a></h2>
<p>Après avoir vérifié le brouillon, sélectionnez <strong>Publier</strong>. Cela rend la conception enregistrée publique et lui donne un numéro de <strong>Version</strong> dans l’historique.</p>
<p>Vous pouvez enregistrer plusieurs points de contrôle pendant la préparation d’une mise à jour, puis publier une seule version terminée. L’historique distingue ces brouillons de travail des versions publiées. Il devient plus facile de retrouver une conception déjà publiée à restaurer.</p>
<p><img decoding="async" loading="lazy" alt="La page d’accueil publiée du club de démonstration accueille les nouveaux curleurs." src="https://curling.io/fr/assets/images/published-result-d679eab368a3e5ff6a54c18fefb023e7.png" width="2560" height="1440" class="img_amcT"></p>
<p>Studio montre aussi qui détient le verrou de modification. Cela empêche deux membres du personnel de modifier le site en même temps. Libérez votre verrou quand vous avez terminé pour qu’une autre personne puisse prendre la relève.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="restore-an-earlier-design">Restaurer une conception antérieure<a href="https://curling.io/fr/blog/website-studio#restore-an-earlier-design" class="hash-link" aria-label="Lien direct vers Restaurer une conception antérieure" title="Lien direct vers Restaurer une conception antérieure" translate="no">​</a></h2>
<p>Ouvrez <strong>Historique des modifications</strong> pour trouver les points de contrôle précédents. Ceux qui ont été publiés montrent aussi leur numéro de version. <strong>Comparer avec le plus récent</strong> permet d’examiner les différences de configuration technique entre un ancien point de contrôle et le point actuel.</p>
<p><img decoding="async" loading="lazy" alt="L’historique du site montre un point de contrôle non publié, la version publiée et les commandes Comparer avec le plus récent et Restaurer." src="https://curling.io/fr/assets/images/history-4533be9d15d79ea0c77ce04f972a97b7.png" width="2560" height="1440" class="img_amcT"></p>
<p>Pour revenir à une conception antérieure :</p>
<ol>
<li class="">Sélectionnez <strong>Restaurer</strong> à côté du point de contrôle souhaité.</li>
<li class="">Vérifiez la conception restaurée dans l’aperçu.</li>
<li class="">Sélectionnez <strong>Publier</strong> pour en faire la version publique.</li>
</ol>
<p>Restaurer crée un nouveau point de contrôle avec l’ancienne conception. L’historique intermédiaire reste donc accessible. La restauration seule ne change pas le site public.</p>
<p>C’est l’historique du site, pas une sauvegarde de tous les dossiers du club. Restaurer une mise en page n’annule pas les inscriptions, les paiements ou les modifications des articles et des produits qu’elle affiche. Les sections dynamiques continuent d’utiliser les données actuelles du club.</p>
<p>Pour une première modification, essayez d’améliorer une introduction ou d’ajouter un lien utile. Vérifiez la proposition et l’aperçu, puis publiez quand le résultat vous convient.</p>]]></content:encoded>
            <category>v3</category>
            <category>Agents</category>
            <category>Gestion de club</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Pourquoi nous avons choisi Rust pour la version 3]]></title>
            <link>https://curling.io/fr/blog/why-we-chose-rust-for-version-3</link>
            <guid>https://curling.io/fr/blog/why-we-chose-rust-for-version-3</guid>
            <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Pourquoi nous avons changé de direction pendant le développement de Curling IO v3, ce que Rust a amélioré et ce qui nous manque de Gleam et Lustre.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cet article technique sur l’architecture provient du développement de Curling IO v3. Il s’adresse aux ingénieurs logiciels et approfondit Gleam, Rust, SQLite, l’architecture des interfaces côté client et les outils de compilation davantage que nos articles habituels sur le produit.</p></div></div>
<p>Pour Curling IO v3, nous savions que nous voulions quitter la pile Ruby on Rails utilisée pour la version 2. Nous voulions un langage à typage statique avec de meilleures <a href="https://xkcd.com/303/" target="_blank" rel="noopener noreferrer">garanties à la compilation</a> et moins de pièges. Nous avons d’abord exploré Gleam, qui répondait à ces critères et nous donnait accès à la BEAM. Lustre offrait un cadre d’interface typé pour le serveur et le client. Une première mise en œuvre nous a permis de voir comment cette combinaison s’intégrerait à notre architecture. Nos premiers articles sur les fondations décrivent ce que nous apprenions et construisions.</p>
<p>Ce travail a aidé à préciser les besoins du produit, les changements souhaités par rapport à la version 2 et la conception de l’interface. Il a aussi révélé des compromis de déploiement et d’interactions côté client qui nous ont finalement amenés à choisir Rust et TypeScript pour notre architecture, avant d’aller plus loin avec Gleam et Lustre. SQLite et la conception du produit ont été conservés. Nous avons depuis porté et enrichi nos outils SQL et construit un petit environnement d’exécution TypeScript appelé <a href="https://github.com/pairshaped/hypertea" target="_blank" rel="noopener noreferrer" class="">Hypertea</a> pour les interactions côté client.</p>
<p>Ce changement a amélioré le déploiement, nous a donné une automatisation utile à la compilation et convenait à notre façon d’examiner et de corriger les problèmes. Malheureusement, il a aussi rendu nos compilations beaucoup plus lourdes. Il y a <a class="" href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#what-we-miss-about-gleam">des aspects de Gleam et Lustre que nous préférons encore</a>.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-we-kept-from-our-initial-exploration">Ce que nous avons gardé de notre première exploration<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#what-we-kept-from-our-initial-exploration" class="hash-link" aria-label="Lien direct vers Ce que nous avons gardé de notre première exploration" title="Lien direct vers Ce que nous avons gardé de notre première exploration" translate="no">​</a></h2>
<p>Les besoins du produit, les changements de comportement prévus par rapport à la version 2, la conception de l’interface et le CSS ont été conservés. SQLite aussi. Gleam et Lustre nous ont également aidés à définir l’architecture et la conception voulues. Cette exploration a inspiré ce que nous avons ensuite construit avec Rust et Hypertea.</p>
<p>Nous avons remplacé tout le code Gleam écrit jusque-là. Il n’en reste aucun dans l’application. Utiliser le même langage sur le serveur et le client, l’environnement d’exécution BEAM et Lustre font partie de ce à quoi nous avons renoncé.</p>
<p>Operations, notre système de suivi des erreurs et l’infrastructure NixOS sont arrivés après le passage à Rust.</p>
<p>Nos premiers articles sur les fondations décrivent la conception Gleam de l’époque. <a class="" href="https://curling.io/fr/blog/automate-club-management-with-ai">Automatiser la gestion du club avec l’IA</a> est aussi issu de ce travail initial.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="deploying-gleam-with-sqlite-was-awkward">Déployer Gleam avec SQLite était compliqué<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#deploying-gleam-with-sqlite-was-awkward" class="hash-link" aria-label="Lien direct vers Déployer Gleam avec SQLite était compliqué" title="Lien direct vers Déployer Gleam avec SQLite était compliqué" translate="no">​</a></h2>
<p>SQLite fonctionnait très bien pour nous. Préparer sa dépendance native pour la version distribuable Erlang était plus frustrant.</p>
<p>Notre intégration SQLite utilisait un NIF, l’interface d’Erlang vers le code natif. Construire une version distribuable exigeait de gérer ce composant natif et sa compatibilité avec l’environnement de production. Il nous fallait un environnement de compilation Linux assez proche de la production et assez puissant pour la compilation et l’édition de liens. Notre configuration exigeait aussi des composants supplémentaires sur l’hôte de production. Nous n’aimions pas entretenir tout cela simplement pour que SQLite fonctionne dans une version distribuable.</p>
<p>Rust a encore des dépendances natives, dont le code C de SQLite, mais nous compilons les versions Linux nativement sur notre machine de développement, les vérifions et téléversons le paquet. La production n’a besoin ni de la chaîne d’outils Rust ni du code source de l’application.</p>
<p>Ce parcours de compilation pour l’autre environnement nous permet aussi de choisir explicitement la cible de la version.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="we-barely-took-advantage-of-the-beam">Nous utilisions à peine les avantages de la BEAM<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#we-barely-took-advantage-of-the-beam" class="hash-link" aria-label="Lien direct vers Nous utilisions à peine les avantages de la BEAM" title="Lien direct vers Nous utilisions à peine les avantages de la BEAM" translate="no">​</a></h2>
<p>L’application avait besoin d’un état durable des tâches dans SQLite, quel que soit l’environnement qui exécutait le travail. Les tâches en attente devaient survivre à la perte d’un processus ou d’une machine. L’environnement d’exécution ne pouvait pas remplacer ce besoin de persistance.</p>
<p>La BEAM offrait toujours la concurrence et la gestion des processus, mais nous n’utilisions plus assez de ses capacités distinctives pour en faire le facteur décisif. Rust avec Tokio répondait aux besoins de concurrence de notre application tout en convenant au parcours de déploiement préféré.</p>
<p>Tokio ne reproduit ni la supervision OTP ni l’isolation des processus de la BEAM. Pour cette charge de travail, nous étions à l’aise d’y renoncer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="rust-is-excellent-for-tooling-and-debugging">Rust excelle pour les outils et le débogage<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#rust-is-excellent-for-tooling-and-debugging" class="hash-link" aria-label="Lien direct vers Rust excelle pour les outils et le débogage" title="Lien direct vers Rust excelle pour les outils et le débogage" translate="no">​</a></h2>
<p>Rust et Gleam partagent plusieurs garde-fous qui facilitent la compréhension d’un code : types statiques, valeurs facultatives explicites et filtrage exhaustif. Les agents de programmation bénéficiaient de ces vérifications dans les deux langages. Deux autres éléments favorisaient Rust dans notre expérience.</p>
<p>À l’époque, les agents proposaient généralement des corrections Rust plus idiomatiques. Ils pouvaient produire du code fonctionnel dans les deux langages, mais le code Rust ressemblait plus souvent à celui d’un développeur qui connaît bien le langage.</p>
<p>Les diagnostics détaillés du compilateur Rust ont aidé les agents à vérifier leurs corrections. Les renseignements d’erreur à l’exécution dans notre pile nous ont aussi aidés à retracer les échecs. Ces diagnostics d’exécution dépendent de nos bibliothèques et du contexte d’erreur conservé. Avec les indications du compilateur, ils aident un agent à suivre un parcours inconnu et à vérifier une correction proposée. Du point de vue d’un agent de programmation, c’est peut-être la plus grande force de Rust.</p>
<p>Les macros de dérivation ont aussi beaucoup compté. <a href="https://serde.rs/" target="_blank" rel="noopener noreferrer" class="">Serde</a> a été très fiable pour nous. Ses dérivations produisent les implémentations de sérialisation et de désérialisation à partir des types Rust sans que nous entretenions ce code répétitif à la main. Nos outils de contrats s’appuient sur ces types pour produire le côté TypeScript des interfaces d’îlots clients.</p>
<p><a href="https://github.com/pairshaped/marmot-rust" target="_blank" rel="noopener noreferrer" class="">Rust Marmot</a> produit des appels de base de données typés à partir de SQL. C’était au départ une <a href="https://github.com/pairshaped/marmot" target="_blank" rel="noopener noreferrer" class="">bibliothèque Gleam</a>. Elle nous plaisait tellement que nous l’avons portée en Rust, puis continué à l’enrichir et à l’améliorer. Les macros de dérivation Rust ont aussi amélioré Marmot. Sa dérivation <code>FromSqlRow</code> produit les correspondances courantes entre les lignes de requête et les types de l’application, tandis que les conversions métier restent explicites.</p>
<p><a href="https://github.com/pairshaped/proute-rust" target="_blank" rel="noopener noreferrer" class="">Rust Proute</a> a suivi un parcours semblable : conçu en Gleam, puis porté en Rust. Il combine la génération de routes et des paramètres typés, y compris la désérialisation dérivée. La génération de routes et le décodage dérivé remplissent des rôles différents et réduisent tous deux le code de liaison à garder synchronisé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="maintaining-the-generators">Entretenir les générateurs<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#maintaining-the-generators" class="hash-link" aria-label="Lien direct vers Entretenir les générateurs" title="Lien direct vers Entretenir les générateurs" translate="no">​</a></h2>
<p>Marmot et Proute produisent du code applicatif qu’il faudrait autrement écrire et synchroniser nous-mêmes. Marmot produit des appels typés au-dessus de rusqlite; Proute génère le routage. Garder ces rôles précis nous aide à éviter une abstraction plus large que nécessaire. Nos <a href="https://github.com/pairshaped/gleam_sqlite_benchmarks/blob/master/REPORT.md" target="_blank" rel="noopener noreferrer" class="">bancs d’essai SQLite publiés</a> ont aussi mesuré un meilleur débit pour Rust Marmot que pour SQLx avec les formes de requêtes testées.</p>
<p>Le code produit est simple. La mécanique derrière ne l’est pas toujours. Marmot doit examiner le schéma de la base de données et transformer les résultats SQL en liaisons typées. Cela exige beaucoup de code, dont l’entretien nous appartient. La génération et les vérifications du compilateur repèrent rapidement de nombreuses erreurs d’interface; elles ne prouvent pas qu’une requête applique la bonne règle métier.</p>
<p>Nous jugeons cette charge gérable. Les générateurs sont documentés et testés, et les diagnostics Rust aident les développeurs comme les agents à y travailler. L’application a des ADR, des règles de responsabilité et des conventions vérifiées pour guider les modifications. Ces éléments, agents compris, aident à intégrer un nouveau développeur. Le jugement appartient toujours à la personne responsable de la maintenance.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="sqlite-was-already-fast-with-gleam-but-rust-nearly-doubled-it">SQLite était déjà rapide avec Gleam, mais Rust a presque doublé son débit<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#sqlite-was-already-fast-with-gleam-but-rust-nearly-doubled-it" class="hash-link" aria-label="Lien direct vers SQLite était déjà rapide avec Gleam, mais Rust a presque doublé son débit" title="Lien direct vers SQLite était déjà rapide avec Gleam, mais Rust a presque doublé son débit" translate="no">​</a></h2>
<p>Nous étions déjà satisfaits des performances de SQLite dans Gleam lorsque nous avons mesuré les deux intégrations.</p>
<p>Nos <a href="https://github.com/pairshaped/gleam_sqlite_benchmarks/blob/master/REPORT.md" target="_blank" rel="noopener noreferrer" class="">bancs d’essai publiés sur les formes de requêtes</a> comparent directement les intégrations :</p>
<table><thead><tr><th>Parcours de base de données</th><th style="text-align:right">Gleam Marmot</th><th style="text-align:right">Rust Marmot</th><th style="text-align:right">Débit Rust/Gleam</th></tr></thead><tbody><tr><td>Séquence d’administration surtout en lecture</td><td style="text-align:right">8 065/s</td><td style="text-align:right">12 886/s</td><td style="text-align:right">1,60×</td></tr><tr><td>Courte transaction de mise à jour</td><td style="text-align:right">21 408/s</td><td style="text-align:right">45 815/s</td><td style="text-align:right">2,14×</td></tr></tbody></table>
<p>Ces tests utilisaient des données synthétiques, des formes de requêtes tirées de la production et un serveur Ryzen 7 9700X avec 62 Gio de mémoire vive. La séquence de lecture exécute environ 26 petites requêtes indexées. La séquence d’écriture exécute une courte transaction avec WAL et <code>synchronous=NORMAL</code>.</p>
<p>Le parcours Rust Marmot utilise <code>rusqlite</code>. Ces mesures couvrent les intégrations complètes de base de données, y compris leurs liaisons et leur code généré. Elles n’isolent pas le coût du NIF et ne mesurent pas le débit de l’application complète. Les gains s’appliquent aux formes de requêtes testées.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="server-components-didnt-remove-the-need-for-client-state">Les composants serveur n’éliminaient pas le besoin d’état côté client<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#server-components-didnt-remove-the-need-for-client-state" class="hash-link" aria-label="Lien direct vers Les composants serveur n’éliminaient pas le besoin d’état côté client" title="Lien direct vers Les composants serveur n’éliminaient pas le besoin d’état côté client" translate="no">​</a></h2>
<p>Un composant serveur était pratique jusqu’à ce que l’état doive changer immédiatement sous la main de la personne.</p>
<p>Un panneau ou un menu déroulant géré par le serveur peut fonctionner, mais l’aller-retour n’est pas toujours agréable. Le glisser-déposer rend le problème plus évident : le pointeur et la réaction visuelle doivent rester ensemble. Envoyer chaque position au serveur et attendre une mise à jour du DOM n’était pas le modèle souhaité.</p>
<p>Nous utilisions des îlots clients Lustre dédiés pour les interactions importantes. Le créateur de formulaires, par exemple, avait un hôte serveur sous <code>server/src/server/admin/product/</code> et son programme de navigateur sous <code>client/src/client/form_builder/</code>. Le paquet serveur ciblait Erlang; le paquet client ciblait JavaScript. Une seule fonctionnalité traversait deux arborescences source.</p>
<p>Il y avait aussi de petits comportements locaux autour des formulaires et de la mise en page. Ils s’accumulaient en fragments JavaScript. Nous aimions la structure typée de l’application et n’aimions pas en sortir pour toutes ces petites interactions.</p>
<p>Le modèle de composants serveur aidait l’administration, mais n’offrait pas le même avantage à notre interface MCP pour agents ou à une éventuelle application native. Cela limitait la part du produit qui pouvait en bénéficier.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="keeping-tea-including-for-the-small-stuff">Garder TEA, même pour les petits détails<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#keeping-tea-including-for-the-small-stuff" class="hash-link" aria-label="Lien direct vers Garder TEA, même pour les petits détails" title="Lien direct vers Garder TEA, même pour les petits détails" translate="no">​</a></h2>
<p>Après le passage à Rust, nous voulions garder The Elm Architecture côté client. Nous avons considéré Elm, mais voulions aussi que l’ajout d’un petit îlot coûte assez peu pour un panneau, une commande ou un comportement modeste de formulaire.</p>
<p>Nous avons construit <a href="https://github.com/pairshaped/hypertea" target="_blank" rel="noopener noreferrer" class="">Hypertea</a>, un petit environnement d’exécution TypeScript inspiré de Hyperapp et de TEA. Nous utilisons la même structure pour les petites commandes et les grands îlots : état explicite, messages typés, fonction de mise à jour, effets et abonnements gérés. Même une petite interaction reçoit cette structure plutôt qu’un autre fragment JavaScript isolé.</p>
<p>TypeScript strict et l’analyse statique soutiennent ce modèle. Ils repèrent les erreurs de valeurs facultatives, imposent un traitement exhaustif là où c’est configuré et gardent les effets secondaires non gérés hors des modules applicatifs ordinaires. Ce ne sont pas toutes les garanties de Gleam ou d’Elm, et les outils sont moins élégants. Nous trouvons quand même cette façon d’écrire les comportements clients bien meilleure que l’entretien de gestionnaires d’événements dispersés dans l’interface.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-rust-build-bill">Le coût des compilations Rust<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#the-rust-build-bill" class="hash-link" aria-label="Lien direct vers Le coût des compilations Rust" title="Lien direct vers Le coût des compilations Rust" translate="no">​</a></h2>
<p>Les compilations ont été pénibles. À mesure que l’application grandissait, une petite modification Rust pouvait exiger une compilation coûteuse de la grosse crate applicative. Un changement de gabarit Maud est du code source Rust. Un changement de requête peut exiger de régénérer les liaisons et de recompiler l’application. Les dépendances en cache peuvent rester à jour, mais l’unité de compilation de l’application demeure grande.</p>
<p>Nous avons désactivé les renseignements de débogage et la LTO en développement. Nous utilisons la compilation incrémentale, limitons les tâches Cargo, sérialisons les vérifications de worktrees concurrents et nettoyons les sorties incrémentales périmées. Ces mesures ont réduit nos temps de compilation, mais pas beaucoup.</p>
<p>Lors d’un essai, un seul processus du compilateur pour un test de bibliothèque a utilisé plus de 10 Gio de mémoire physique sur un portable de 16 Gio. Même les essais de compilation des tests avec une seule tâche ont dépassé un seuil d’arrêt de 8 Gio. Un autre relevé a trouvé des répertoires de compilation de worktree entre 5,7 et 7,9 Gio. C’étaient des artefacts et des caches accumulés, pas la taille de l’exécutable déployé.</p>
<p>Toucher un gabarit d’administration sans changer son contenu a déclenché un <code>cargo check</code> de 21,38 secondes. Cela ne comprenait ni l’édition de liens du serveur, ni son redémarrage, ni l’inspection du résultat. C’était une borne inférieure favorable : le contenu source n’avait pas changé, donc le compilateur pouvait réutiliser plus de travail qu’après une vraie modification du balisage. Pour des retouches répétées de l’interface, le délai devient difficile à ignorer.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="an-extraction-that-helped">Une extraction utile<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#an-extraction-that-helped" class="hash-link" aria-label="Lien direct vers Une extraction utile" title="Lien direct vers Une extraction utile" translate="no">​</a></h3>
<p>Nos générateurs applicatifs vivaient initialement dans le même paquet Cargo que l’application. Les exécuter compilait du code applicatif dont ils n’avaient pas besoin, et une fonction de génération de contrats créait une autre identité de bibliothèque applicative.</p>
<p>Nous avons déplacé les générateurs dans leur propre petit paquet. Dans une comparaison d’août, les temps et l’espace disque ont changé ainsi :</p>
<table><thead><tr><th>Parcours de génération</th><th style="text-align:right">Avant l’extraction</th><th style="text-align:right">Après l’extraction</th></tr></thead><tbody><tr><td>Parcours à froid</td><td style="text-align:right">237,47 secondes</td><td style="text-align:right">16,25 secondes</td></tr><tr><td>Parcours à chaud</td><td style="text-align:right">54,28 secondes</td><td style="text-align:right">3,53 secondes</td></tr><tr><td>Sortie de compilation après génération</td><td style="text-align:right">4,28 Gio</td><td style="text-align:right">257 Mio</td></tr></tbody></table>
<p>La sortie générée est restée identique. La génération est devenue beaucoup moins coûteuse, même si compiler la grosse application reste cher.</p>
<p>Nous avons aussi étudié le cache de compilation avec <code>sccache</code>. L’admissibilité au cache et sa réutilisation diffèrent entre les dépendances à froid et les modifications incrémentales de l’application. Ajouter un cache ne corrige donc pas automatiquement ces dernières. Il faut mesurer la boucle dans laquelle nous passons réellement du temps.</p>
<p>Avec plusieurs worktrees d’agents, chaque copie de travail accumule aussi son état de compilation. Sérialiser Cargo protège le portable contre les compilateurs simultanés, mais introduit une file d’attente. Une machine de compilation dédiée pourrait éventuellement en valoir la peine. Le coût de compilation nous pousse déjà à revoir le découpage de l’application et l’endroit où nous produisons l’interface.</p>
<p>Nous espérons que séparer des parties de l’application en crates Rust réduira les coûts de compilation. Nous n’avons pas encore établi cet avantage. L’extraction des générateurs a amélioré un parcours précis; l’effet d’un découpage supplémentaire sur les modifications applicatives quotidiennes reste à déterminer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-we-miss-about-gleam">Ce qui nous manque de Gleam<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#what-we-miss-about-gleam" class="hash-link" aria-label="Lien direct vers Ce qui nous manque de Gleam" title="Lien direct vers Ce qui nous manque de Gleam" translate="no">​</a></h2>
<p>Gleam est un langage agréable à utiliser. Les valeurs facultatives explicites, le filtrage exhaustif et un langage petit et cohérent donnent des garde-fous utiles sans rendre le code applicatif ordinaire pénible à écrire. Nous le trouvons encore beaucoup plus agréable à lire et à écrire que Rust. Nous nous sommes habitués à la syntaxe Rust, mais préférons encore celle de Gleam.</p>
<p><a href="https://lustre.build/" target="_blank" rel="noopener noreferrer" class="">Lustre</a> convenait particulièrement bien. Il apportait The Elm Architecture à Gleam : un modèle, des messages, une fonction de mise à jour et une vue. Nous pouvions utiliser cette structure dans les applications de navigateur et les composants serveur, avec des types Gleam partout. Il y avait peu de JavaScript écrit à la main et aucun langage de gabarit distinct à apprendre.</p>
<p>Nous avons décrit l’administration dans <a class="" href="https://curling.io/fr/blog/live-admin-without-javascript">Une administration interactive avec Gleam et Lustre</a>. Un processus BEAM de longue durée conservait l’état de la page, traitait les messages et envoyait les mises à jour du DOM par WebSocket. Pour les formulaires, les tableaux et les pages de configuration, c’était une façon productive de construire une interface.</p>
<p>L’accès à la BEAM était un autre avantage important. Ses processus, sa supervision et ses capacités à l’exécution sont de bonnes raisons de choisir cette pile. Dans notre expérience, la boucle de compilation Gleam était aussi considérablement plus légère que celle de notre application Rust, maintenant beaucoup plus grande.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="in-conclusion">En conclusion<a href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3#in-conclusion" class="hash-link" aria-label="Lien direct vers En conclusion" title="Lien direct vers En conclusion" translate="no">​</a></h2>
<p>Gleam et Lustre auraient probablement bien fonctionné pour nous. Nous sommes heureux d’avoir choisi Rust et TypeScript : ils conviennent mieux à notre architecture et à notre façon de travailler, malgré le coût des compilations et ce qui nous manque. La souplesse d’Hypertea a aussi porté ses fruits de façons inattendues, avec <a href="https://github.com/pairshaped/hypertea#webmcp" target="_blank" rel="noopener noreferrer" class="">WebMCP</a> qui s’appuie directement sur sa conception TEA existante.</p>]]></content:encoded>
            <category>v3</category>
            <category>Fondations</category>
            <category>Architecture</category>
            <category>Gleam</category>
            <category>Rust</category>
            <category>SQLite</category>
            <category>Hypertea</category>
        </item>
        <item>
            <title><![CDATA[Une intervention humaine encadrée par des contrats]]></title>
            <link>https://curling.io/fr/blog/human-in-the-loop-with-contracts</link>
            <guid>https://curling.io/fr/blog/human-in-the-loop-with-contracts</guid>
            <pubDate>Tue, 25 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Pourquoi Curling IO fige les propositions des agents dans des contrats applicatifs typés, demande à une personne d’en approuver les effets exacts et les exécute sans redonner le contrôle au modèle.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cette note technique décrit l’architecture de l’assistant IA dans Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aux autres personnes qui conçoivent des systèmes d’agents. Elle approfondit Rust, la persistance, l’autorisation et le traitement des échecs davantage que nos articles habituels sur le produit.</p></div></div>
<p>Le schéma habituel d’intervention humaine dans un agent IA ressemble à ceci : le modèle demande un appel d’outil, l’environnement d’exécution de l’agent s’arrête, une personne approuve l’appel, puis l’exécution reprend pour permettre à l’outil de s’exécuter.</p>
<p>C’est une conception généraliste raisonnable. Elle est aussi plus stricte que de laisser simplement un agent appeler tous les outils qu’il voit. Pour Curling IO, nous voulions exposer au modèle la plus petite surface possible et placer un garde-fou contrôlé par l’application sur chaque chemin menant à une écriture. Cela nous a amenés à poser une question plus stricte :</p>
<p>Si l’application possède déjà les détails exacts de l’appel, pourquoi redonner le contrôle à l’agent?</p>
<p>Au moment où nous demandons à un gestionnaire de club d’approuver une opération, Curling IO a analysé la requête du modèle, résolu chaque valeur par défaut, vérifié l’état actuel de l’application, produit un aperçu fixe et conservé les arguments exacts. Le modèle n’a plus rien d’utile à apporter à l’exécution. Nous ne le laissons donc pas exécuter l’opération et ne le relançons pas simplement pour donner suite à l’approbation.</p>
<p>L’agent propose. L’application transforme cette proposition en contrat. Une personne approuve le contrat. Rust l’exécute.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-common-approval-pattern">Le schéma d’approbation courant<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#the-common-approval-pattern" class="hash-link" aria-label="Lien direct vers Le schéma d’approbation courant" title="Lien direct vers Le schéma d’approbation courant" translate="no">​</a></h2>
<p>Le <a href="https://openai.github.io/openai-agents-python/human_in_the_loop/" target="_blank" rel="noopener noreferrer" class="">parcours d’intervention humaine du SDK OpenAI Agents</a> peut suspendre une exécution lorsqu’un outil exige une approbation. L’application sérialise le <code>RunState</code>, enregistre les approbations ou les refus pour les appels d’outils en attente, puis reprend l’exécution initiale. L’outil approuvé s’exécute pendant cette reprise.</p>
<p>Les <a href="https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/review-tool-calls/" target="_blank" rel="noopener noreferrer" class="">interruptions de LangGraph</a> suivent une structure semblable. Un graphe peut s’arrêter avant un nœud d’outil, laisser une personne approuver, modifier ou refuser l’appel, puis reprendre vers l’outil ou revenir au modèle.</p>
<p>Ces cadres répondent à un problème général. Un agent peut avoir de nombreux outils, des agents imbriqués, du travail de longue durée et plusieurs points où une personne doit intervenir. Dans ce contexte, conserver l’appel d’outil en attente dans un état durable de l’agent est utile.</p>
<p>Le problème de Curling IO est plus restreint. Nous contrôlons l’application, la base de données, les règles d’autorisation, l’interface et chaque opération qu’un assistant peut proposer. Nous n’avons pas besoin qu’un environnement d’exécution générique d’agents conserve l’autorité après qu’une proposition est devenue un état de l’application.</p>
<p>Voici la différence :</p>
<p><img decoding="async" loading="lazy" alt="Comparaison entre l’approbation habituelle d’un appel d’outil, où l’exécution de l’agent reprend après l’approbation humaine, et les contrats d’opération Curling IO, où Rust contrôle le contrat validé et l’exécute sans relancer le modèle." src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjcyMCIgdmlld0JveD0iMCAwIDEyMDAgNzIwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzY3JpcHRpb24iPgogIDx0aXRsZSBpZD0idGl0bGUiPkNvbXBhcmlzb24gb2YgdG9vbC1jYWxsIGFwcHJvdmFsIGFuZCBDdXJsaW5nIElPIG9wZXJhdGlvbiBjb250cmFjdHM8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjcmlwdGlvbiI+SW4gYSBjb21tb24gdG9vbC1jYWxsIGFwcHJvdmFsIGZsb3csIHRoZSBtb2RlbCByZXF1ZXN0cyBhIHRvb2wsIHRoZSBhZ2VudCBydW50aW1lIHBhdXNlcywgYSBodW1hbiBhcHByb3ZlcywgdGhlIHJ1bnRpbWUgcmVzdW1lcywgYW5kIHRoZSB0b29sIGV4ZWN1dGVzLiBJbiBDdXJsaW5nIElPLCB0aGUgbW9kZWwgcHJvcG9zZXMgYW4gb3BlcmF0aW9uLCBSdXN0IHZhbGlkYXRlcyBhbmQgZnJlZXplcyBhIGNvbnRyYWN0LCBhIGh1bWFuIGFwcHJvdmVzIGl0cyBleGFjdCBlZmZlY3RzLCB0aGVuIFJ1c3QgcmVsb2FkcywgcmV2YWxpZGF0ZXMsIGV4ZWN1dGVzLCBhbmQgc3RvcmVzIHRoZSBvdXRjb21lIHdpdGhvdXQgcmVzdW1pbmcgdGhlIG1vZGVsLjwvZGVzYz4KICA8ZGVmcz4KICAgIDxmaWx0ZXIgaWQ9InNoYWRvdyIgeD0iLTIwJSIgeT0iLTIwJSIgd2lkdGg9IjE0MCUiIGhlaWdodD0iMTQwJSI+CiAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjAiIGR5PSIyIiBzdGREZXZpYXRpb249IjMiIGZsb29kLWNvbG9yPSIjMGYxNzJhIiBmbG9vZC1vcGFjaXR5PSIuMTIiLz4KICAgIDwvZmlsdGVyPgogICAgPG1hcmtlciBpZD0iYXJyb3ctbXV0ZWQiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOCIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPgogICAgICA8cGF0aCBkPSJNIDAgMCBMIDEwIDUgTCAwIDEwIHoiIGZpbGw9IiM2NDc0OGIiLz4KICAgIDwvbWFya2VyPgogICAgPG1hcmtlciBpZD0iYXJyb3ctYmx1ZSIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+CiAgICAgIDxwYXRoIGQ9Ik0gMCAwIEwgMTAgNSBMIDAgMTAgeiIgZmlsbD0iIzI1NjNlYiIvPgogICAgPC9tYXJrZXI+CiAgPC9kZWZzPgoKICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI3MjAiIHJ4PSIyMCIgZmlsbD0iI2Y4ZmFmYyIvPgogIDx0ZXh0IHg9IjQ4IiB5PSI1NCIgZmlsbD0iIzBmMTcyYSIgZm9udC1mYW1pbHk9IkludGVyLCB1aS1zYW5zLXNlcmlmLCBzeXN0ZW0tdWksIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzAiIGZvbnQtd2VpZ2h0PSI3MDAiPldobyBvd25zIHRoZSBvcGVyYXRpb24gYWZ0ZXIgYXBwcm92YWw/PC90ZXh0PgoKICA8ZyBmb250LWZhbWlseT0iSW50ZXIsIHVpLXNhbnMtc2VyaWYsIHN5c3RlbS11aSwgc2Fucy1zZXJpZiI+CiAgICA8cmVjdCB4PSIzNiIgeT0iODgiIHdpZHRoPSIxMTI4IiBoZWlnaHQ9IjI1MCIgcng9IjE2IiBmaWxsPSIjZmZmIiBzdHJva2U9IiNjYmQ1ZTEiIHN0cm9rZS13aWR0aD0iMiIvPgogICAgPHRleHQgeD0iNjQiIHk9IjEyNiIgZmlsbD0iIzBmMTcyYSIgZm9udC1zaXplPSIyMSIgZm9udC13ZWlnaHQ9IjcwMCI+Q29tbW9uIHRvb2wtY2FsbCBhcHByb3ZhbDwvdGV4dD4KICAgIDx0ZXh0IHg9IjY0IiB5PSIxNTMiIGZpbGw9IiM0NzU1NjkiIGZvbnQtc2l6ZT0iMTYiPlRoZSBwYXVzZWQgYWdlbnQgcnVuIHJlbWFpbnMgdGhlIGNvb3JkaW5hdG9yLjwvdGV4dD4KCiAgICA8ZyBmaWx0ZXI9InVybCgjc2hhZG93KSI+CiAgICAgIDxyZWN0IHg9IjY0IiB5PSIxODgiIHdpZHRoPSIxODAiIGhlaWdodD0iMTA0IiByeD0iMTIiIGZpbGw9IiNmOGZhZmMiIHN0cm9rZT0iI2NiZDVlMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxyZWN0IHg9IjI4NiIgeT0iMTg4IiB3aWR0aD0iMTgwIiBoZWlnaHQ9IjEwNCIgcng9IjEyIiBmaWxsPSIjZjhmYWZjIiBzdHJva2U9IiNjYmQ1ZTEiIHN0cm9rZS13aWR0aD0iMiIvPgogICAgICA8cmVjdCB4PSI1MDgiIHk9IjE4OCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMDQiIHJ4PSIxMiIgZmlsbD0iI2ZmZjdlZCIgc3Ryb2tlPSIjZmI5MjNjIiBzdHJva2Utd2lkdGg9IjIiLz4KICAgICAgPHJlY3QgeD0iNzMwIiB5PSIxODgiIHdpZHRoPSIxODAiIGhlaWdodD0iMTA0IiByeD0iMTIiIGZpbGw9IiNmOGZhZmMiIHN0cm9rZT0iI2NiZDVlMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxyZWN0IHg9Ijk1MiIgeT0iMTg4IiB3aWR0aD0iMTgwIiBoZWlnaHQ9IjEwNCIgcng9IjEyIiBmaWxsPSIjZjhmYWZjIiBzdHJva2U9IiNjYmQ1ZTEiIHN0cm9rZS13aWR0aD0iMiIvPgogICAgPC9nPgoKICAgIDxnIGZpbGw9IiMwZjE3MmEiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtd2VpZ2h0PSI3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPgogICAgICA8dGV4dCB4PSIxNTQiIHk9IjIyNCI+PHRzcGFuIHg9IjE1NCI+TW9kZWwgcmVxdWVzdHM8L3RzcGFuPjx0c3BhbiB4PSIxNTQiIGR5PSIyMyI+YSB0b29sIGNhbGw8L3RzcGFuPjwvdGV4dD4KICAgICAgPHRleHQgeD0iMzc2IiB5PSIyMjQiPjx0c3BhbiB4PSIzNzYiPkFnZW50IHJ1bnRpbWU8L3RzcGFuPjx0c3BhbiB4PSIzNzYiIGR5PSIyMyI+cGF1c2VzPC90c3Bhbj48L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjU5OCIgeT0iMjI0Ij48dHNwYW4geD0iNTk4Ij5IdW1hbiByZXZpZXdzPC90c3Bhbj48dHNwYW4geD0iNTk4IiBkeT0iMjMiPmFuZCBhcHByb3ZlczwvdHNwYW4+PC90ZXh0PgogICAgICA8dGV4dCB4PSI4MjAiIHk9IjIyNCI+PHRzcGFuIHg9IjgyMCI+QWdlbnQgcnVudGltZTwvdHNwYW4+PHRzcGFuIHg9IjgyMCIgZHk9IjIzIj5yZXN1bWVzPC90c3Bhbj48L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjEwNDIiIHk9IjIyNCI+PHRzcGFuIHg9IjEwNDIiPkFwcHJvdmVkIHRvb2w8L3RzcGFuPjx0c3BhbiB4PSIxMDQyIiBkeT0iMjMiPmV4ZWN1dGVzPC90c3Bhbj48L3RleHQ+CiAgICA8L2c+CgogICAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ3NDhiIiBzdHJva2Utd2lkdGg9IjMiIG1hcmtlci1lbmQ9InVybCgjYXJyb3ctbXV0ZWQpIj4KICAgICAgPHBhdGggZD0iTTI0NCAyNDBoMzEiLz4KICAgICAgPHBhdGggZD0iTTQ2NiAyNDBoMzEiLz4KICAgICAgPHBhdGggZD0iTTY4OCAyNDBoMzEiLz4KICAgICAgPHBhdGggZD0iTTkxMCAyNDBoMzEiLz4KICAgIDwvZz4KCiAgICA8cmVjdCB4PSIzNiIgeT0iMzcwIiB3aWR0aD0iMTEyOCIgaGVpZ2h0PSIzMDQiIHJ4PSIxNiIgZmlsbD0iI2VmZjZmZiIgc3Ryb2tlPSIjOTNjNWZkIiBzdHJva2Utd2lkdGg9IjIiLz4KICAgIDx0ZXh0IHg9IjY0IiB5PSI0MDgiIGZpbGw9IiMwZjE3MmEiIGZvbnQtc2l6ZT0iMjEiIGZvbnQtd2VpZ2h0PSI3MDAiPkN1cmxpbmcgSU8gb3BlcmF0aW9uIGNvbnRyYWN0PC90ZXh0PgogICAgPHRleHQgeD0iNjQiIHk9IjQzNSIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNiI+VGhlIHZhbGlkYXRlZCBwcm9wb3NhbCBiZWNvbWVzIGR1cmFibGUgYXBwbGljYXRpb24gc3RhdGUuIFRoZSBtb2RlbCBydW4gaXMgZmluaXNoZWQuPC90ZXh0PgoKICAgIDxwYXRoIGQ9Ik0yNjMgNDUxdjE4MiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMjU2M2ViIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjcgNyIvPgogICAgPHJlY3QgeD0iMjc4IiB5PSI0NDgiIHdpZHRoPSIyMjYiIGhlaWdodD0iMzAiIHJ4PSIxNSIgZmlsbD0iI2RiZWFmZSIvPgogICAgPHRleHQgeD0iMzkxIiB5PSI0NjkiIGZpbGw9IiMxZDRlZDgiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkFQUExJQ0FUSU9OLU9XTkVEIEZST00gSEVSRTwvdGV4dD4KCiAgICA8ZyBmaWx0ZXI9InVybCgjc2hhZG93KSI+CiAgICAgIDxyZWN0IHg9IjY0IiB5PSI1MDQiIHdpZHRoPSIxODAiIGhlaWdodD0iMTEyIiByeD0iMTIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2NiZDVlMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxyZWN0IHg9IjI4NiIgeT0iNTA0IiB3aWR0aD0iMTgwIiBoZWlnaHQ9IjExMiIgcng9IjEyIiBmaWxsPSIjZGJlYWZlIiBzdHJva2U9IiM2MGE1ZmEiIHN0cm9rZS13aWR0aD0iMiIvPgogICAgICA8cmVjdCB4PSI1MDgiIHk9IjUwNCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMTIiIHJ4PSIxMiIgZmlsbD0iI2ZmZjdlZCIgc3Ryb2tlPSIjZmI5MjNjIiBzdHJva2Utd2lkdGg9IjIiLz4KICAgICAgPHJlY3QgeD0iNzMwIiB5PSI1MDQiIHdpZHRoPSIxODAiIGhlaWdodD0iMTEyIiByeD0iMTIiIGZpbGw9IiNkYmVhZmUiIHN0cm9rZT0iIzYwYTVmYSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxyZWN0IHg9Ijk1MiIgeT0iNTA0IiB3aWR0aD0iMTgwIiBoZWlnaHQ9IjExMiIgcng9IjEyIiBmaWxsPSIjZGJlYWZlIiBzdHJva2U9IiMyNTYzZWIiIHN0cm9rZS13aWR0aD0iMiIvPgogICAgPC9nPgoKICAgIDxnIGZpbGw9IiMwZjE3MmEiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtd2VpZ2h0PSI3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPgogICAgICA8dGV4dCB4PSIxNTQiIHk9IjU0MCI+PHRzcGFuIHg9IjE1NCI+TW9kZWwgcHJvcG9zZXM8L3RzcGFuPjx0c3BhbiB4PSIxNTQiIGR5PSIyMyI+YW4gb3BlcmF0aW9uPC90c3Bhbj48L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjM3NiIgeT0iNTMwIj48dHNwYW4geD0iMzc2Ij5SdXN0IHZhbGlkYXRlczwvdHNwYW4+PHRzcGFuIHg9IjM3NiIgZHk9IjIzIj5hbmQgZnJlZXplczwvdHNwYW4+PHRzcGFuIHg9IjM3NiIgZHk9IjIzIj50aGUgY29udHJhY3Q8L3RzcGFuPjwvdGV4dD4KICAgICAgPHRleHQgeD0iNTk4IiB5PSI1MzAiPjx0c3BhbiB4PSI1OTgiPkh1bWFuIGFwcHJvdmVzPC90c3Bhbj48dHNwYW4geD0iNTk4IiBkeT0iMjMiPnRoZSBleGFjdDwvdHNwYW4+PHRzcGFuIHg9IjU5OCIgZHk9IjIzIj5lZmZlY3RzPC90c3Bhbj48L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjgyMCIgeT0iNTMwIj48dHNwYW4geD0iODIwIj5SdXN0IHJlbG9hZHM8L3RzcGFuPjx0c3BhbiB4PSI4MjAiIGR5PSIyMyI+YW5kIHJldmFsaWRhdGVzPC90c3Bhbj48dHNwYW4geD0iODIwIiBkeT0iMjMiPmN1cnJlbnQgc3RhdGU8L3RzcGFuPjwvdGV4dD4KICAgICAgPHRleHQgeD0iMTA0MiIgeT0iNTMwIj48dHNwYW4geD0iMTA0MiI+UnVzdCBleGVjdXRlczwvdHNwYW4+PHRzcGFuIHg9IjEwNDIiIGR5PSIyMyI+YW5kIHN0b3JlcyB0aGU8L3RzcGFuPjx0c3BhbiB4PSIxMDQyIiBkeT0iMjMiPnR5cGVkIG91dGNvbWU8L3RzcGFuPjwvdGV4dD4KICAgIDwvZz4KCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiMyNTYzZWIiIHN0cm9rZS13aWR0aD0iMyIgbWFya2VyLWVuZD0idXJsKCNhcnJvdy1ibHVlKSI+CiAgICAgIDxwYXRoIGQ9Ik0yNDQgNTYwaDMxIi8+CiAgICAgIDxwYXRoIGQ9Ik00NjYgNTYwaDMxIi8+CiAgICAgIDxwYXRoIGQ9Ik02ODggNTYwaDMxIi8+CiAgICAgIDxwYXRoIGQ9Ik05MTAgNTYwaDMxIi8+CiAgICA8L2c+CgogICAgPHRleHQgeD0iMTA0MiIgeT0iNjQ3IiBmaWxsPSIjMWQ0ZWQ4IiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNzAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5OTyBNT0RFTCBSRVNVTUU8L3RleHQ+CiAgPC9nPgo8L3N2Zz4K" width="1200" height="720" class="img_amcT"></p>
<p>Ne pas relancer le modèle après l’approbation a un sens précis. Pendant la préparation d’une proposition, les erreurs de validation qui peuvent être communiquées sans risque peuvent retourner dans la boucle active du modèle pour qu’il corrige sa requête. Si un gestionnaire refuse une proposition, ou si l’exécution constate un changement d’état récupérable, Curling IO enregistre un contexte de révision rédigé par l’application pour le prochain message du gestionnaire. Cela lance un nouveau tour du modèle avec un motif qui peut lui être communiqué sans risque. Un échec interne terminal est plutôt enregistré et signalé comme non réessayable. Le modèle peut s’adapter lorsque c’est utile, mais il ne contrôle jamais l’exécution du contrat approuvé.</p>
<p>Le premier schéma peut être mis en œuvre de façon sûre. Relancer un agent ne modifie pas automatiquement un appel approuvé. Un bon environnement d’exécution devrait conserver l’appel exact et son identité. Pour une application qui maîtrise ses propres opérations, l’exécution suspendue du modèle est un état opérationnel superflu dès que l’application a accepté une proposition complète.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="human-approval-is-not-enough">L’approbation humaine ne suffit pas<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#human-approval-is-not-enough" class="hash-link" aria-label="Lien direct vers L’approbation humaine ne suffit pas" title="Lien direct vers L’approbation humaine ne suffit pas" translate="no">​</a></h2>
<p>Un bouton d’approbation n’a de sens que si l’application peut dire exactement ce qui a été approuvé.</p>
<p>Supposons qu’un modèle demande de rembourser une commande. Une approbation peu précise pourrait afficher :</p>
<blockquote>
<p>Rembourser ce client?</p>
</blockquote>
<p>Presque toutes les décisions importantes restent alors cachées. Quel paiement? Quel montant? Quelle ligne de commande? Où ira l’argent? Le modèle utilise-t-il une valeur qu’il a calculée lui-même? Pourrait-il choisir une autre destination à la reprise de l’exécution?</p>
<p>Notre vérification de remboursement montre le participant, le produit, le rabais, le paiement, le montant, la destination et le nouveau total de la commande. Le gestionnaire n’approuve pas l’intention générale du modèle de corriger une commande. Il approuve une opération concrète et un ensemble précis d’effets.</p>
<p>Cela exige plus qu’un schéma d’outil. Il faut un contrat contrôlé par l’application qui définit ensemble :</p>
<ul>
<li class="">les champs que le modèle doit fournir;</li>
<li class="">les valeurs par défaut que Rust peut résoudre;</li>
<li class="">les combinaisons valides et invalides;</li>
<li class="">la représentation typée conservée pour approbation;</li>
<li class="">la présentation fixe que la personne vérifie;</li>
<li class="">la revalidation selon l’état actuel;</li>
<li class="">l’exécuteur;</li>
<li class="">le résultat typé et sa restitution sûre;</li>
<li class="">les événements d’audit de tout le cycle de vie.</li>
</ul>
<p>Si ces éléments sont répartis entre une consigne, un schéma JSON générique, une page de vérification construite à la main et un exécuteur sans lien avec eux, ils finiront par diverger. Un nouveau champ peut influer sur l’exécution sans apparaître dans la vérification. Une consigne peut décrire une valeur par défaut autrement que l’application. Un modèle peut produire une valeur que l’interface ordinaire n’autoriserait jamais.</p>
<p>Dans Curling IO, tous ces éléments font partie d’une même capacité.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-smallest-useful-surface">La plus petite surface utile<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#the-smallest-useful-surface" class="hash-link" aria-label="Lien direct vers La plus petite surface utile" title="Lien direct vers La plus petite surface utile" translate="no">​</a></h2>
<p>Notre premier choix est d’éviter de présenter une large surface à l’agent, avant même de chercher à la protéger.</p>
<p>Chaque assistant est limité à une section de Curling IO et ne reçoit que le contexte nécessaire à la tâche en cours. L’assistant des commandes ne reçoit pas une vue de la base de données de toute l’organisation. L’assistant des courriels n’hérite pas des outils de celui des commandes. L’identité de l’organisation, les permissions, les noms internes de champs, les charges utiles des fournisseurs et les dossiers de clients sans rapport avec la tâche restent du côté de l’application.</p>
<p>La même règle s’applique aux opérations. Un assistant voit un petit catalogue de propositions possibles, plutôt qu’une API d’écriture générique. Les champs présentés au modèle contiennent seulement les choix qui exigent réellement une interprétation. Rust fournit la portée des ressources, résout les valeurs par défaut, calcule les valeurs dérivées, refuse les combinaisons non prises en charge et construit l’aperçu destiné à la personne.</p>
<p>Nous ajoutons ensuite un garde-fou sur chaque voie restante entre la sortie du modèle et l’état durable :</p>
<ul>
<li class="">un contexte limité et rédigé par l’application avant l’appel au modèle;</li>
<li class="">des outils de lecture propres à la tâche, dont le serveur contrôle la portée par organisation;</li>
<li class="">une analyse et une validation strictes de la requête d’opération du modèle;</li>
<li class="">des arguments et des aperçus typés construits par Rust;</li>
<li class="">l’approbation humaine de chaque effet important;</li>
<li class="">la vérification de l’autorisation, de l’expiration et de l’état au moment de l’exécution;</li>
<li class="">l’idempotence aux frontières de la proposition et de l’enregistrement métier;</li>
<li class="">des résultats typés, avec les renseignements sensibles masqués, après l’exécution.</li>
</ul>
<p>Aucune vérification ne garantit à elle seule toute la sécurité. L’approbation humaine ne remplace pas l’autorisation. Un type ne prouve pas que l’état actuel permet encore l’opération. La revalidation n’empêche pas un appel en double au fournisseur après une réponse perdue. La surface reste petite, et chaque frontière conserve son rôle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-model-request-is-not-the-stored-proposal">La requête du modèle n’est pas la proposition conservée<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#the-model-request-is-not-the-stored-proposal" class="hash-link" aria-label="Lien direct vers La requête du modèle n’est pas la proposition conservée" title="Lien direct vers La requête du modèle n’est pas la proposition conservée" translate="no">​</a></h2>
<p>Notre assistant des commandes a une opération présentée au modèle appelée <code>propose_payment_refund</code>. Son entrée est volontairement petite. Le modèle identifie le paiement, la ligne de commande, le rabais et la destination demandée à partir des éléments fournis par Curling IO.</p>
<p>Rust ne conserve pas directement cette requête. Il la valide à partir de l’examen de la commande contrôlé par le serveur, calcule le remboursement selon les règles de l’application, résout une destination concrète et crée des arguments typés :</p>
<div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token attribute attr-name" style="color:rgb(255, 203, 107)">#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">struct</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">OrderRefundArguments</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    order_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    payment_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    line_item_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    discount_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    amount_cents</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    refund_destination</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">RefundDestination</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Le modèle ne choisit pas <code>amount_cents</code>. Il ne peut pas dire « utiliser le mode de paiement initial » et remettre cette décision à plus tard. Rust résout cette expression en une valeur concrète de <code>RefundDestination</code> avant que le gestionnaire voie quoi que ce soit.</p>
<p>La présentation à vérifier a un type distinct des arguments exécutables :</p>
<div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token attribute attr-name" style="color:rgb(255, 203, 107)">#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">struct</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">OrderRefundPreview</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    participant_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    product_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    discount_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    payment_method</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">PaymentMethod</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    amount_cents</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    current_order_total_cents</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    resulting_order_total_cents</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">i64</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    currency</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    refund_destination</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">RefundDestination</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    product_configuration_unchanged</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">bool</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Les arguments contiennent ce dont l’exécution a besoin. L’aperçu contient ce dont une personne a besoin pour comprendre les conséquences. Les deux proviennent des mêmes faits métier validés et sont figés ensemble.</p>
<p>JSON apparaît à la frontière de la base de données, mais ce n’est pas le modèle de programmation. Une opération est reconvertie dans son type Rust avant de pouvoir être revalidée ou exécutée. Un type d’opération ne peut pas être aiguillé vers le type d’arguments d’une autre opération.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="a-proposal-is-durable-application-state">Une proposition est un état durable de l’application<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#a-proposal-is-durable-application-state" class="hash-link" aria-label="Lien direct vers Une proposition est un état durable de l’application" title="Lien direct vers Une proposition est un état durable de l’application" translate="no">​</a></h2>
<p>Chaque proposition reçoit un identifiant public opaque et une ligne qui contient son organisation, l’administrateur demandeur, la section, la ressource, le type d’opération, les arguments exacts, l’aperçu, l’état et l’expiration. Elle conserve aussi les renseignements sur l’approbation, l’exécution, le résultat et les échecs.</p>
<p>Voici une vue simplifiée du côté Rust :</p>
<div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">struct</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">OperationProposal</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">Arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Preview</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Result</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    public_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">ProposalId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    interaction_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">InteractionId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    scope</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">OperationScope</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    requested_by</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">UserId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    operation_kind</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">OperationKind</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    preview</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Preview</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    status</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">ProposalStatus</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    expires_at</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">DateTime</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    approval</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Option</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">Approval</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    outcome</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Option</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">OperationOutcome</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">Result</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;&gt;</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">struct</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">OperationScope</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    organization_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">OrganizationId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    section</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">AssistantSection</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    resource_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">ResourceId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p><code>Arguments</code>, <code>Preview</code> et <code>Result</code> sont les types propres à un contrat d’opération. SQLite conserve leur forme sérialisée et les événements d’audit de la proposition, mais le code de l’application ne peut pas exécuter ces valeurs sans les décoder avec le contrat correspondant.</p>
<p>La conversation n’est pas la source de vérité de cette opération. L’état d’exécution conservé par le fournisseur de modèles ne l’est pas non plus. Une proposition en attente survit à une déconnexion du navigateur, à un changement de modèle ou à un déploiement, puisque Curling IO peut reconstruire l’approbation à partir de ses propres enregistrements.</p>
<p>Cela réduit aussi beaucoup l’entrée de la demande d’approbation. Le navigateur transmet l’identifiant opaque de la proposition. Il ne transmet pas une seconde fois le montant du remboursement, sa destination, le corps du message ou toute autre valeur approuvée.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="approval-creates-one-execution-contract">L’approbation crée un seul contrat d’exécution<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#approval-creates-one-execution-contract" class="hash-link" aria-label="Lien direct vers L’approbation crée un seul contrat d’exécution" title="Lien direct vers L’approbation crée un seul contrat d’exécution" translate="no">​</a></h2>
<p>Lorsque l’administrateur sélectionne <strong>Approuver le remboursement</strong>, Curling IO vérifie de nouveau toutes les conditions habituelles de la requête :</p>
<ul>
<li class="">la personne connectée a toujours accès à l’organisation;</li>
<li class="">la proposition appartient à cette organisation, à cette section et à cette commande;</li>
<li class="">la proposition est encore en attente et n’a pas expiré;</li>
<li class="">son type d’opération et ses arguments conservés peuvent encore être analysés;</li>
<li class="">la personne actuelle est toujours autorisée à effectuer l’opération sous-jacente.</li>
</ul>
<p>Seulement alors, la proposition passe de <code>pending</code> à <code>executing</code>. La mise à jour est atomique et produit un contrat d’exécution typé :</p>
<div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">struct</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">ExecutionContract</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">Arguments</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    proposal_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">ProposalId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    operation_kind</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">OperationKind</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    scope</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">OperationScope</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    approved_by</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">UserId</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    approved_at</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">DateTime</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">match</span><span class="token plain"> proposals</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">approve_for_execution</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">proposal_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> administrator</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> now</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">Approval</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token class-name" style="color:rgb(255, 203, 107)">Claimed</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">contract</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">execute</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">contract</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">Approval</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token class-name" style="color:rgb(255, 203, 107)">AlreadyCompleted</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">outcome</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">present</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">outcome</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">Approval</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token class-name" style="color:rgb(255, 203, 107)">NotApprovable</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">reason</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">reject</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">reason</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Le dépôt de données ne retourne <code>Approval::Claimed</code> que lorsqu’il fait passer de façon atomique la proposition correspondante, non expirée, de <code>pending</code> à <code>executing</code>. Si une autre requête l’a déjà approuvée, refusée ou terminée, le gestionnaire de requête ne reçoit aucun contrat d’exécution. Il ne reçoit donc pas une seconde autorisation de l’exécuter.</p>
<p>Le modèle ne participe à aucune de ces étapes. L’approbation est une requête authentifiée de l’administrateur à Curling IO, plutôt qu’un autre message dans la conversation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="revalidation-is-part-of-execution">La revalidation fait partie de l’exécution<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#revalidation-is-part-of-execution" class="hash-link" aria-label="Lien direct vers La revalidation fait partie de l’exécution" title="Lien direct vers La revalidation fait partie de l’exécution" translate="no">​</a></h2>
<p>Figer une proposition empêche ses arguments de changer. Cela ne fige pas le reste du monde.</p>
<p>Un paiement a peut-être été remboursé dans un autre onglet. Quelqu’un a pu modifier le public d’une diffusion. L’administrateur a peut-être perdu son accès. Une proposition peut être restée ouverte assez longtemps pour expirer.</p>
<p>L’exécuteur de remboursement recharge donc la commande et prouve que les éléments initiaux restent valides. Il vérifie que :</p>
<ul>
<li class="">la même ligne de commande et les mêmes éléments justifiant le rabais existent encore;</li>
<li class="">le montant calculé n’a pas changé;</li>
<li class="">le paiement sélectionné existe encore et sa valeur remboursable est suffisante;</li>
<li class="">aucune tentative de remboursement non résolue ne rend un nouvel appel risqué;</li>
<li class="">la destination concrète reste compatible avec le paiement et le compte.</li>
</ul>
<p>Si un fait important a changé, l’ancienne proposition est bloquée par défaut. L’application enregistre un résultat qui exige une révision, explique le changement sans exposer de renseignements sensibles et demande au gestionnaire de préparer une nouvelle proposition. Elle ne modifie pas discrètement le montant sous une approbation qui en affichait un autre.</p>
<p>C’est le contrôle de concurrence optimiste exprimé en termes humains. L’aperçu affirme quelque chose sur un état précis. La revalidation prouve que cette affirmation est encore vraie lorsque l’approbation arrive.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="idempotency-has-to-reach-the-domain-record">L’idempotence doit aller jusqu’à l’enregistrement métier<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#idempotency-has-to-reach-the-domain-record" class="hash-link" aria-label="Lien direct vers L’idempotence doit aller jusqu’à l’enregistrement métier" title="Lien direct vers L’idempotence doit aller jusqu’à l’enregistrement métier" translate="no">​</a></h2>
<p>L’approbation conditionnelle empêche une même proposition en attente de démarrer deux fois, mais cela ne suffit pas pour les opérations qui ont des effets externes.</p>
<p>Prenons un remboursement de carte en ligne. Curling IO peut envoyer la requête au fournisseur et perdre la réponse HTTP. À ce stade, réessayer peut créer un second remboursement. Le résultat correct n’est pas un échec générique. C’est <code>OperationOutcome::NeedsReconciliation { status_path }</code>.</p>
<p>Le parcours de remboursement conserve des identités stables pour la proposition et le fournisseur, enregistre la tentative non résolue et refuse d’improviser un autre appel. La récupération vérifie la tentative initiale et finit par enregistrer le résultat faisant autorité.</p>
<p>Pour les opérations locales de base de données, la modification métier et le résultat réussi de la proposition sont validés dans la même transaction. Pour les opérations externes, la proposition reste dans un état d’exécution ou de rapprochement jusqu’à ce que la récupération lève l’incertitude.</p>
<p>Approuver de nouveau une proposition terminée retourne son résultat initial conservé. Cela ne fabrique pas un nouveau message de réussite et n’exécute pas l’opération une seconde fois. La répétition concerne la livraison du résultat, pas un nouveau résultat métier.</p>
<p>Cette distinction compte, puisque les réponses HTTP ne sont pas durables. L’enregistrement de l’opération l’est.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="handing-back-the-result-without-resuming-the-agent">Restituer le résultat sans relancer l’agent<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#handing-back-the-result-without-resuming-the-agent" class="hash-link" aria-label="Lien direct vers Restituer le résultat sans relancer l’agent" title="Lien direct vers Restituer le résultat sans relancer l’agent" translate="no">​</a></h2>
<p>L’application sait déjà ce qui s’est passé. Elle ne devrait donc pas dépenser un autre appel au modèle pour lui demander de le reformuler.</p>
<p>Nous utilisons un petit vocabulaire de résultats typés autour du résultat propre à chaque opération :</p>
<div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">enum</span><span class="token plain"> </span><span class="token type-definition class-name" style="color:rgb(255, 203, 107)">OperationOutcome</span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;</span><span class="token class-name" style="color:rgb(255, 203, 107)">S</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">Succeeded</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token class-name" style="color:rgb(255, 203, 107)">S</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">NeedsReconciliation</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        status_path</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">RevisionRequired</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        reason</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">RevisionReason</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token class-name" style="color:rgb(255, 203, 107)">TerminalFailure</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        correlation_id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">String</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>Le <code>S</code> propre à l’opération peut identifier le remboursement et le solde de commande qui en résulte, ou indiquer qu’un brouillon de courriel a été copié dans le formulaire modifiable. L’énumération commune indique ce que la personne et le système peuvent faire ensuite sans risque.</p>
<p>Un résultat conservé alimente plusieurs présentations :</p>
<ul>
<li class="">une réponse localisée dans le navigateur;</li>
<li class="">un suivi rédigé par l’application dans la conversation de l’assistant;</li>
<li class="">un contexte limité fourni si le gestionnaire envoie un autre message;</li>
<li class="">éventuellement, le même résultat sémantique par une API destinée aux agents logiciels.</li>
</ul>
<p>Le modèle ne classe pas l’échec et ne décide pas si une nouvelle tentative est sûre. Rust s’en charge. Au prochain tour de la personne, un résultat récupérable donne au modèle assez de contexte sûr pour examiner de nouveau la situation ou préparer une proposition révisée.</p>
<p>Pour une violation d’invariant interne ou un contrat conservé corrompu, l’application enregistre elle-même l’erreur opérationnelle. Le modèle et le navigateur reçoivent un message terminal avec un identifiant de corrélation, sans trace de pile ni invitation à continuer d’essayer. Demander à l’agent de soumettre un billet de soutien ajouterait une étape sujette aux échecs tout en écartant le contexte que l’application possède déjà.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="two-assistants-were-enough-to-expose-the-boundary">Deux assistants ont suffi à faire ressortir la frontière<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#two-assistants-were-enough-to-expose-the-boundary" class="hash-link" aria-label="Lien direct vers Deux assistants ont suffi à faire ressortir la frontière" title="Lien direct vers Deux assistants ont suffi à faire ressortir la frontière" translate="no">​</a></h2>
<p>Nous avons actuellement deux contrats d’opération, volontairement différents.</p>
<p>L’assistant des commandes peut proposer <code>order.refund_missing_discount</code>. L’approbation peut mener à une opération financière avec un fournisseur externe, des réponses incertaines et du travail de rapprochement.</p>
<p>L’assistant des diffusions de courriel peut proposer <code>email_broadcast.apply_draft</code>. L’approbation copie les filtres, l’objet et le message figés dans un formulaire modifiable. Elle ne crée pas la diffusion et ne l’envoie pas. C’est une opération locale, mais elle recompte quand même les destinataires avant d’appliquer le brouillon. Si le public a changé, le gestionnaire doit obtenir une nouvelle proposition.</p>
<p>Cette différence nous a empêchés de construire un cadre de remboursement et de le présenter comme un cadre d’agents. La partie commune est petite : cycle de vie, identité d’approbation, sémantique des résultats, audit et restitution. Les types d’arguments, les aperçus, la revalidation et l’exécution restent dans l’opération qui les comprend.</p>
<p>Nous prévoyons ajouter des assistants à quelques dizaines de sections. Chaque nouvelle opération nécessitera une requête typée, des arguments figés, un aperçu destiné à une personne, un revalidateur, un exécuteur, un résultat typé et des tests ciblés pour l’expiration, l’état périmé, la répétition et la classification des échecs.</p>
<p>C’est plus de travail que d’ajouter un autre outil de fonction à une consigne. C’est voulu.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-this-pattern-is-and-what-it-is-not">Ce que ce schéma apporte et ses limites<a href="https://curling.io/fr/blog/human-in-the-loop-with-contracts#what-this-pattern-is-and-what-it-is-not" class="hash-link" aria-label="Lien direct vers Ce que ce schéma apporte et ses limites" title="Lien direct vers Ce que ce schéma apporte et ses limites" translate="no">​</a></h2>
<p>Nous n’avons inventé ni l’approbation humaine, ni les commandes durables, ni le contrôle de concurrence optimiste, ni la sécurité par capacités, ni les clés d’idempotence. La conception emprunte à tous ces concepts. Les cadres d’agents permettent déjà de suspendre les appels d’outils pour vérification.</p>
<p>Le changement utile consiste à traiter la modification demandée par l’agent comme une entrée d’une commande applicative, plutôt que comme la commande elle-même. L’application matérialise un nouvel objet durable dont la sémantique est plus stricte que celle de l’appel au modèle à son origine. Dès que cet objet existe, on peut abandonner l’exécution du modèle.</p>
<p>Ce schéma n’est pas nécessaire pour chaque réponse d’assistant. Les réponses en lecture seule n’ont pas besoin de propositions figées. Un brouillon sans effet sur l’application peut rester un brouillon. Mais si une opération change les données d’un client, envoie quelque chose, déplace de l’argent ou modifie un accès, nous voulons une garantie plus forte que « le modèle a appelé un outil et une personne a cliqué sur Approuver ».</p>
<p>Nous voulons savoir exactement ce que l’application s’est engagée à faire, exactement ce que la personne a approuvé, exactement quels faits actuels ont été revérifiés et exactement ce qui s’est passé ensuite.</p>
<p>C’est le contrat.</p>]]></content:encoded>
            <category>v3</category>
            <category>Agents</category>
            <category>Rust</category>
            <category>Architecture</category>
            <category>Sécurité</category>
        </item>
        <item>
            <title><![CDATA[L’assistance intégrée dans Curling IO]]></title>
            <link>https://curling.io/fr/blog/in-app-assistance-in-curling-io</link>
            <guid>https://curling.io/fr/blog/in-app-assistance-in-curling-io</guid>
            <pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Comment les assistants facultatifs de Curling IO, spécialisés par tâche, peuvent examiner un problème, préparer une opération et l’exécuter après approbation.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Curling IO v3 comprend une assistance intégrée facultative pour la plupart des tâches d’un gestionnaire de club. Elle apparaît dans la section où le travail se fait et utilise le contexte nécessaire à ce travail.</p>
<p>Un assistant peut examiner un problème, expliquer ses constatations et préparer une opération à vérifier. Il exécute cette opération seulement après l’approbation explicite d’un administrateur.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="a-change-from-our-earlier-plan">Un changement par rapport à notre plan initial<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#a-change-from-our-earlier-plan" class="hash-link" aria-label="Lien direct vers Un changement par rapport à notre plan initial" title="Lien direct vers Un changement par rapport à notre plan initial" translate="no">​</a></h2>
<p>Plus tôt cette année, nous avons <a class="" href="https://curling.io/fr/blog/automate-club-management-with-ai">écrit sur la possibilité pour les clubs de connecter leur propre agent IA</a> à Curling IO. Cette approche laissait le choix et la configuration du modèle au club. Nous avons depuis changé de direction. L’expérience principale passera par des assistants intégrés à Curling IO, avec un fournisseur de modèles externe que nous choisissons et gérons.</p>
<p>Gérer nous-mêmes le fournisseur nous permet de :</p>
<ul>
<li class="">contrôler précisément les renseignements et les opérations accessibles pour chaque tâche;</li>
<li class="">exiger du fournisseur de modèles une <strong>politique de conservation nulle des données</strong>;</li>
<li class="">éviter de demander à chaque club de configurer et de payer son propre compte de modèles.</li>
</ul>
<p>Le fonctionnement devrait aussi coûter moins cher, puisque chaque assistant reçoit le contexte dont il a besoin au lieu de devoir découvrir le fonctionnement du système entier. Lorsqu’un assistant a besoin d’un meilleur contexte, nous pouvons améliorer sa documentation de référence et la conception de sa tâche pour tous les clubs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="investigating-an-order">Examiner une commande<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#investigating-an-order" class="hash-link" aria-label="Lien direct vers Examiner une commande" title="Lien direct vers Examiner une commande" translate="no">​</a></h2>
<p><em>Nous avons créé cette situation dans un club de démonstration, puis l’avons traitée avec l’assistant intégré. Les réponses présentées ci-dessous n’étaient pas préparées à l’avance.</em></p>
<p>Supposons qu’un club offre un rabais Jeunesse de 50 % aux curleurs de 17 ans et moins. Ce rabais est attribué à la plupart des ligues du club, mais un administrateur en oublie une pendant la préparation de la saison.</p>
<p>Madison s’inscrit à cette ligue et la commande est payée au plein tarif. Plus tard, son parent appelle le club. Le gestionnaire ouvre la commande et demande :</p>
<blockquote>
<p>Le parent de Madison a appelé. Il a payé le plein tarif, mais pensait que Madison devait recevoir le rabais Jeunesse. Peux-tu vérifier?</p>
</blockquote>
<p>L’assistant peut consulter les renseignements pertinents à cette commande. Il vérifie l’âge de Madison au début de la saison, les conditions du rabais Jeunesse, la ligue achetée et les rabais qui lui sont attribués.</p>
<p><img decoding="async" loading="lazy" alt="L’assistant des commandes examine pourquoi l’inscription payée de Madison n’a pas reçu le rabais Jeunesse." src="https://curling.io/fr/assets/images/order-investigation-d649a255d955ec91bf8910439bd2c7eb.png" width="2400" height="1440" class="img_amcT"></p>
<p>Dans ce cas, Madison respecte la condition d’âge. Le problème vient du fait que le rabais Jeunesse n’était pas attribué à la ligue. Il n’a donc pas été pris en compte dans le calcul du prix de la commande.</p>
<p>Cette distinction compte. Le parcours de commande a suivi la configuration du club. L’assistant a trouvé une erreur de configuration probable, mais ne présume pas que le club voulait appliquer le rabais. Le gestionnaire le confirme avant toute modification.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="preparing-the-correction">Préparer la correction<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#preparing-the-correction" class="hash-link" aria-label="Lien direct vers Préparer la correction" title="Lien direct vers Préparer la correction" translate="no">​</a></h2>
<p>Après que le gestionnaire a confirmé que la ligue devrait offrir le rabais Jeunesse, l’assistant calcule l’ajustement avec les mêmes règles de tarification que le parcours de commande. Il prépare une réduction de la commande et le remboursement correspondant sur le paiement initial. Pour l’inscription de Madison à 275 $, le remboursement proposé est de 137,50 $.</p>
<p><img decoding="async" loading="lazy" alt="Une proposition de remboursement pour la commande de Madison montre le participant, la ligue, le rabais Jeunesse, le paiement à rembourser, la destination et le montant du remboursement, le nouveau total de la commande et le bouton Approuver le remboursement." src="https://curling.io/fr/assets/images/order-refund-proposal-31c4e31120e9a46170ba752c385b9306.png" width="2400" height="1440" class="img_amcT"></p>
<p>Le gestionnaire peut vérifier le calcul, le paiement à rembourser, la destination du remboursement et le solde de la commande qui en résultera. Rien ne change avant qu’il approuve l’opération. Si le remboursement doit plutôt aller au crédit du compte, il peut refuser la proposition et demander ce changement.</p>
<p>L’approbation corrige cette commande. Elle ne change pas la configuration de la ligue. L’assistant explique que le gestionnaire doit encore ouvrir le produit et lui attribuer le rabais Jeunesse pour que les prochaines inscriptions en bénéficient.</p>
<p>Après l’approbation, la commande montre la réduction de 137,50 $ et le remboursement sur le paiement initial.</p>
<p><img decoding="async" loading="lazy" alt="La commande de Madison après l’approbation, avec la réduction de 137,50 $ et le remboursement enregistrés sur le paiement initial." src="https://curling.io/fr/assets/images/order-refund-completed-c6f7feeea356a584748b29c457c1a182.png" width="2496" height="1440" class="img_amcT"></p>
<p>Cette limite est voulue. L’assistant des commandes peut examiner et corriger cette commande. Il ne peut pas se mettre à modifier un produit parce que son examen a révélé un problème de configuration du produit.</p>
<p>Nous pourrions éventuellement permettre un transfert explicite de l’assistant des commandes à celui des produits. Pour le moment, chaque assistant reste dans un environnement strictement limité à sa tâche.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="preparing-an-email-broadcast">Préparer une diffusion de courriel<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#preparing-an-email-broadcast" class="hash-link" aria-label="Lien direct vers Préparer une diffusion de courriel" title="Lien direct vers Préparer une diffusion de courriel" translate="no">​</a></h2>
<p><em>Nous avons créé cette situation dans un club de démonstration, puis l’avons traitée avec l’assistant intégré. Les réponses présentées ci-dessous n’étaient pas préparées à l’avance.</em></p>
<p>Un gestionnaire de club peut aussi demander à l’assistant de préparer un courriel pour un public précis. L’assistant prépare les filtres de destinataires et le message, puis présente la diffusion à vérifier.</p>
<p>Par exemple, un gestionnaire peut demander :</p>
<blockquote>
<p>Prépare un court rappel pour toutes les personnes inscrites aux Doubles mixtes du mardi cette saison. Indique que la première séance aura lieu le mardi 7 octobre à 19 h et demande-leur d’arriver 15 minutes à l’avance.</p>
</blockquote>
<p>L’assistant teste les filtres demandés séparément du formulaire de diffusion, trouve quatre destinataires correspondants et prépare l’objet et le message. Il présente ensuite le brouillon complet à vérifier. Le formulaire du gestionnaire n’a pas encore changé.</p>
<p><img decoding="async" loading="lazy" alt="Une proposition de diffusion de courriel montre le public, les quatre destinataires actuels, l’objet, le message et les boutons Refuser et Appliquer à la diffusion, tandis que le formulaire modifiable reste inchangé." src="https://curling.io/fr/assets/images/email-broadcast-prepared-7790ce09541cda25f8cbb5697c2d89b0.png" width="2400" height="1440" class="img_amcT"></p>
<p>Si le gestionnaire choisit <strong>Appliquer à la diffusion</strong>, Curling IO copie ces filtres et ce message exacts dans le formulaire modifiable. Le gestionnaire peut encore les modifier, vérifier les destinataires obtenus et utiliser la confirmation d’envoi habituelle lorsqu’il est prêt. Cette étape ne crée pas la diffusion et ne l’envoie pas.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="limited-to-the-current-task">Une portée limitée à la tâche en cours<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#limited-to-the-current-task" class="hash-link" aria-label="Lien direct vers Une portée limitée à la tâche en cours" title="Lien direct vers Une portée limitée à la tâche en cours" translate="no">​</a></h2>
<p>Chaque assistant est facultatif et reste inactif tant que personne ne l’utilise. Lorsqu’un gestionnaire demande de l’aide, Curling IO rassemble seulement les renseignements pertinents pour accomplir cette tâche et les fournit au modèle sous une forme limitée et structurée.</p>
<p>L’exemple de commande n’exige pas un accès illimité au club. Il nécessite la commande, les renseignements sur le participant et l’achat qui concernent son prix, ainsi que la configuration des rabais liée à la question. Les commandes et les renseignements sur les membres sans rapport avec cette tâche sont exclus.</p>
<p>Curling IO détermine aussi les outils propres à la tâche qui sont accessibles dans chaque section. L’assistant des commandes peut examiner les prix et proposer une correction de commande. Il ne peut pas modifier un produit. L’assistant des diffusions de courriel reçoit un autre ensemble d’outils de lecture seule pour préparer un public et un message et en afficher l’aperçu.</p>
<p>Le modèle ne peut pas appeler du code arbitraire de l’application ni ajouter des outils à sa tâche. Surtout, il ne reçoit aucun outil qui exécute une opération approuvée.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="data-processing">Traitement des données<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#data-processing" class="hash-link" aria-label="Lien direct vers Traitement des données" title="Lien direct vers Traitement des données" translate="no">​</a></h2>
<p>Curling IO exige que le fournisseur de modèles externe qui traite chaque demande à un assistant applique une politique de conservation nulle des données. Le fournisseur peut donc traiter les renseignements fournis pour la demande et retourner une réponse, mais ne peut ensuite conserver ni la demande ni la réponse, ni les utiliser pour entraîner des modèles. La journalisation des demandes par le fournisseur n’est pas activée.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="approval-is-part-of-the-operation">L’approbation fait partie de l’opération<a href="https://curling.io/fr/blog/in-app-assistance-in-curling-io#approval-is-part-of-the-operation" class="hash-link" aria-label="Lien direct vers L’approbation fait partie de l’opération" title="Lien direct vers L’approbation fait partie de l’opération" translate="no">​</a></h2>
<p>Préparer une opération et l’exécuter sont deux étapes distinctes. L’assistant peut présenter une proposition, mais ne peut ni l’approuver ni l’exécuter.</p>
<p>Chaque opération a un contrat qui définit les renseignements requis, les valeurs valides, les valeurs par défaut, la présentation pour vérification et les règles d’exécution. Si l’assistant ne peut pas remplir tous les champs obligatoires à partir des renseignements disponibles et qu’aucune valeur par défaut valide n’existe, il doit recueillir d’autres renseignements ou poser une question au gestionnaire.</p>
<p>La proposition contient l’action exacte et tous les paramètres qui peuvent influer sur le résultat. Curling IO conserve cet instantané et utilise le contrat pour produire la présentation que vérifie le gestionnaire. L’assistant ne décide pas des champs à afficher. Pour le remboursement, la vérification comprend le participant, le produit, le rabais, le paiement à rembourser, le montant et la destination du remboursement, ainsi que le nouveau total de la commande. Pour la diffusion, elle comprend les filtres de destinataires, le nombre actuel de destinataires, l’objet et le message complet.</p>
<p>L’approbation transforme cette proposition figée en <strong>contrat d’exécution</strong>. C’est une requête authentifiée que l’administrateur adresse à Curling IO. Ce n’est pas une demande envoyée au modèle. Le modèle ne reprend pas après l’approbation et ne reçoit jamais de fonction qui puisse exécuter le contrat.</p>
<p>Curling IO charge le contrat conservé et l’exécute dans le code de l’application. Aucun modèle ne fonctionne à cette étape. Ses paramètres ne peuvent donc pas changer entre la vérification et l’exécution. Le contrat est à usage unique. Un lot, une autre action ou tout changement important exige une nouvelle proposition et une autre approbation.</p>
<p>L’approbation ne contourne pas le fonctionnement habituel de l’application. Curling IO vérifie les permissions de l’administrateur, valide l’état actuel et applique les mêmes règles de gestion que l’interface ordinaire. Si l’opération n’est plus valide au moment de l’approbation, elle est refusée plutôt qu’exécutée à partir de renseignements périmés.</p>
<p>Si le gestionnaire choisit <strong>Refuser</strong>, Curling IO enregistre cette décision et lui demande ce qu’il souhaite changer. Son prochain message poursuit la même conversation, avec le refus dans le contexte. La proposition refusée ne peut plus être approuvée par la suite.</p>
<p>La création de la proposition, son approbation, son refus, son exécution et l’enregistrement qui en résulte sont consignés dans un journal d’audit. Les opérations qui peuvent être réessayées conservent aussi l’identité de la proposition pour qu’une requête interrompue ne puisse pas exécuter le même contrat deux fois.</p>
<p>D’ici le lancement de Curling IO v3, nous ajouterons des assistants à d’autres sections. Chacun aura ses propres renseignements et contrats liés à sa tâche. La limite imposée par l’approbation restera la même.</p>]]></content:encoded>
            <category>v3</category>
            <category>Agents</category>
            <category>Gestion de club</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Envoyer les achats Curling IO à Zapier ou n8n]]></title>
            <link>https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n</link>
            <guid>https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n</guid>
            <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Les webhooks de Curling IO v3 peuvent envoyer les achats de produits sélectionnés, y compris les locations, à Zapier, n8n ou un autre point de terminaison HTTPS.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Les clubs doivent souvent envoyer des renseignements sur les achats à des systèmes externes à Curling IO. Il peut s’agir de mettre à jour un tableur, d’aviser le personnel ou de lancer un processus dans un autre service.</p>
<p>Les locations en sont un exemple. Quelqu’un peut devoir préparer le salon, réserver des tables, organiser un service de traiteur ou ajouter la réservation au tableur du personnel.</p>
<p>Les webhooks de Curling IO v3 peuvent envoyer cet achat de produit à <a href="https://zapier.com/" target="_blank" rel="noopener noreferrer" class="">Zapier</a>, <a href="https://n8n.io/" target="_blank" rel="noopener noreferrer" class="">n8n</a> ou un autre point de terminaison HTTPS lorsqu’il est soumis, payé, annulé ou déplacé à une autre heure.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="choose-the-products-to-watch">Choisir les produits à surveiller<a href="https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n#choose-the-products-to-watch" class="hash-link" aria-label="Lien direct vers Choisir les produits à surveiller" title="Lien direct vers Choisir les produits à surveiller" translate="no">​</a></h2>
<p>Un webhook envoie un seul type de notification à une seule destination. Un club peut envoyer :</p>
<ul>
<li class="">tous les produits;</li>
<li class="">les produits de certains types sélectionnés, comme les locations;</li>
<li class="">des produits individuels, comme quatre options précises de location pour entreprises.</li>
</ul>
<p>Le filtre de produits offre ces trois portées. Si deux destinataires doivent recevoir des produits différents, l’administrateur crée un webhook distinct pour chaque destinataire et chaque portée.</p>
<p>Les locations et leurs extras sont des produits. La même configuration de webhook fonctionne aussi pour les adhésions, les ligues, les programmes et les autres produits.</p>
<p><img decoding="async" loading="lazy" alt="Un webhook Curling IO configuré pour envoyer les achats de locations payés à Zapier." src="https://curling.io/fr/assets/images/configure-rental-webhook-c241239ea9d1e772b327f79c05e222bd.png" width="2880" height="1800" class="img_amcT"></p>
<p><em>Un webhook peut envoyer tous les produits, certains types de produits ou des produits individuels. Celui-ci envoie les achats de locations payés à Zapier.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="choose-when-to-send">Choisir le moment de l’envoi<a href="https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n#choose-when-to-send" class="hash-link" aria-label="Lien direct vers Choisir le moment de l’envoi" title="Lien direct vers Choisir le moment de l’envoi" translate="no">​</a></h2>
<p>Un webhook utilise un seul déclencheur :</p>
<ul>
<li class=""><strong>Un produit est soumis</strong> après la finalisation de la commande.</li>
<li class=""><strong>Un produit est payé en totalité</strong> la première fois que sa ligne de commande est entièrement payée.</li>
<li class=""><strong>Un produit est annulé</strong> lorsqu’une affectation de remboursement annule la ligne de commande. Réduire sa quantité ou sa valeur n’envoie pas cette notification.</li>
<li class=""><strong>Un produit est replanifié</strong> lorsqu’un administrateur change l’heure de début du produit acheté dans le calendrier.</li>
</ul>
<p>Un processus qui exige plusieurs déclencheurs peut utiliser des webhooks distincts vers la même destination.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="choose-what-the-receiver-gets">Choisir ce que reçoit le destinataire<a href="https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n#choose-what-the-receiver-gets" class="hash-link" aria-label="Lien direct vers Choisir ce que reçoit le destinataire" title="Lien direct vers Choisir ce que reçoit le destinataire" translate="no">​</a></h2>
<p>Le club choisit les champs d’achat envoyés par chaque webhook. Les nouveaux webhooks commencent avec 16 champs recommandés qui couvrent la commande, l’inscription, le produit, les prix et l’horaire. Les renseignements sur l’acheteur et le participant ne sont pas sélectionnés par défaut.</p>
<p>Les champs sont regroupés et peuvent être recherchés. Un administrateur peut sélectionner un groupe entier, le désélectionner ou revenir à la sélection recommandée. L’aperçu de la charge utile se met à jour immédiatement. Le club peut donc voir les clés JSON et des valeurs représentatives avant d’enregistrer ou d’envoyer un test.</p>
<p><img decoding="async" loading="lazy" alt="Un aperçu de la charge utile d’un webhook avec le nom du produit et des champs sensibles sur le participant sélectionnés." src="https://curling.io/fr/assets/images/configure-webhook-payload-4e28a8f8a9b96035e375aba8fe98fbce.png" width="2880" height="1200" class="img_amcT"></p>
<p><em>Cet exemple utilise une petite sélection de champs pour montrer toute la charge utile. Les détails de livraison sont toujours inclus; les autres clés suivent la sélection enregistrée.</em></p>
<p>Les champs sur l’acheteur et le participant portent la mention <strong>Sensible</strong>. Les questions personnalisées sur les participants et les inscriptions portent la mention <strong>Peut être sensible</strong>, puisque cela dépend de la question. Curling IO avertit l’administrateur, mais laisse le club décider si la destination doit recevoir ces renseignements. Les mots de passe, les jetons d’authentification, les données de carte de paiement et les secrets des fournisseurs de paiement ne peuvent pas être sélectionnés.</p>
<p>Curling IO accepte uniquement des URL HTTPS publiques comme destinations, ce qui chiffre la requête pendant son transport. Curling IO ne contrôle pas la façon dont Zapier, n8n ou un autre service destinataire traite les données après leur livraison et n’est pas responsable de ce traitement par un tiers. Le club est responsable du choix du service et des champs qu’il lui envoie.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="test-the-receiver-before-enabling-the-webhook">Tester le destinataire avant d’activer le webhook<a href="https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n#test-the-receiver-before-enabling-the-webhook" class="hash-link" aria-label="Lien direct vers Tester le destinataire avant d’activer le webhook" title="Lien direct vers Tester le destinataire avant d’activer le webhook" translate="no">​</a></h2>
<p>L’administrateur peut laisser un webhook désactivé et sélectionner <strong>Envoyer un test</strong>. Le test utilise la sélection de champs enregistrée et le véritable parcours de livraison. Il contient une location représentative pour que Zapier ou n8n puisse découvrir les champs JSON sélectionnés avant l’activation du webhook.</p>
<p>Dans Zapier, utilisez un déclencheur <strong>Webhooks by Zapier &gt; Catch Hook</strong>. Dans n8n, utilisez un nœud <strong>Webhook</strong>. Copiez l’URL HTTPS du destinataire dans Curling IO, envoyez le test et associez les champs avant d’activer l’automatisation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="delivery-reliability">Fiabilité des livraisons<a href="https://curling.io/fr/blog/product-webhooks-for-zapier-and-n8n#delivery-reliability" class="hash-link" aria-label="Lien direct vers Fiabilité des livraisons" title="Lien direct vers Fiabilité des livraisons" translate="no">​</a></h2>
<p>Les appels de webhook s’exécutent en arrière-plan. Curling IO réessaie jusqu’à cinq fois en cas de panne réseau, de délai d’attente dépassé, de limite de débit ou d’erreur du serveur. Le destinataire peut utiliser l’identifiant stable de publication du webhook pour éliminer un doublon si une requête réussit, mais que sa réponse se perd.</p>
<p>Chaque requête est signée avec HMAC-SHA256 pour les destinataires qui peuvent vérifier la signature. Un déclencheur Catch Hook de Zapier repose normalement plutôt sur son URL difficile à deviner. Cette URL doit donc être traitée comme un mot de passe.</p>
<p>Les administrateurs peuvent consulter les <strong>Livraisons récentes</strong>, voir les tentatives et les codes d’état HTTP, réessayer une livraison échouée encore conservée, renouveler le secret de signature ou désactiver le webhook. Les données détaillées de charge utile et de livraison sont supprimées après 30 jours. Chaque livraison conserve le corps exact de sa requête, et chaque nouvelle tentative renvoie ce même corps, même si la sélection de champs ou les données d’achat changent par la suite.</p>
<p><img decoding="async" loading="lazy" alt="Les livraisons récentes d’un webhook Curling IO, avec des tentatives réussies et échouées." src="https://curling.io/fr/assets/images/recent-deliveries-cf7cd107e59077cd0ce4273f6ae46058.png" width="2880" height="1640" class="img_amcT"></p>
<p><em>Les livraisons récentes montrent les requêtes réussies, les tentatives échouées, les codes d’état HTTP et le moment où une livraison échouée peut être réessayée.</em></p>
<p>Consultez le <a class="" href="https://curling.io/fr/docs/integrations/webhooks">guide de configuration des webhooks</a> pour la charge utile, les étapes dans Zapier et n8n, les règles de nouvelle tentative et la vérification des signatures.</p>]]></content:encoded>
            <category>v3</category>
            <category>Intégrations</category>
            <category>Webhooks</category>
            <category>Locations</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Optimiser les horaires de séances de curling]]></title>
            <link>https://curling.io/fr/blog/optimizing-curling-draw-schedules</link>
            <guid>https://curling.io/fr/blog/optimizing-curling-draw-schedules</guid>
            <pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cet article fait partie de notre série d’aperçus de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Curling IO v3 comprend un nouvel écran de planification des séances pour les parties d’événement. L’horaire est une grille de séances et de ressources du club, avec les parties non attribuées dans une file à côté. Vous pouvez déplacer les parties par glisser-déposer, verrouiller certains placements et utiliser <strong>Répartir</strong> et <strong>Optimiser</strong> en respectant ces verrous.</p>
<p>Contrairement à un générateur de modèles d’horaires distinct, cet éditeur travaille avec les vraies équipes, étapes, parties, ressources et heures de séance de l’événement. Enregistrer l’horaire met directement l’événement à jour.</p>
<p>Vous pouvez déjà essayer la plupart de l’interface sur <a href="https://curlingschedules.com/" target="_blank" rel="noopener noreferrer" class="">CurlingSchedules.com</a>. Elle utilise des équipes génériques et des sauvegardes locales au navigateur plutôt que les parties réelles d’un événement. La grille, le glisser-déposer, les verrous, les horaires du catalogue, l’inspection de l’équité et l’optimisation sont toutefois disponibles dès aujourd’hui.</p>
<!-- -->
<p><img decoding="async" loading="lazy" alt="Un horaire de séances de ligue dans l’éditeur d’événement Curling IO v3" src="https://curling.io/fr/assets/images/canonical-8x4-6e54d901aa66b015304394203545e0eb.png" width="1280" height="720" class="img_amcT"></p>
<p><em>L’éditeur de la version 3 réunit la file de parties, les commandes d’événement, les heures de séance, les ressources et les parties planifiées dans une vue.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-draw-scheduling-tool">L’outil de planification des séances<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#the-draw-scheduling-tool" class="hash-link" aria-label="Lien direct vers L’outil de planification des séances" title="Lien direct vers L’outil de planification des séances" translate="no">​</a></h2>
<p>La version 2 génère des modèles d’horaires et utilise des listes déroulantes pour placer les parties. Elle n’a ni cet éditeur, ni le catalogue canonique complet, ni les verrous de partie ou l’optimisation locale autour des changements manuels.</p>
<p>L’éditeur de la version 3 permet de :</p>
<ul>
<li class="">Déplacer les parties entre l’horaire et la file par glisser-déposer.</li>
<li class="">Modifier les heures, ajouter des séances en plaçant des parties, ou supprimer une séance et remettre ses parties dans la file.</li>
<li class="">Verrouiller une partie pour que <strong>Répartir</strong> et <strong>Optimiser</strong> la laissent en place.</li>
<li class="">Répartir les parties de tournoi à la ronde, les parties ponctuelles et les parties de tableau selon leurs règles d’ordre distinctes, en incluant les parties en attente sur demande.</li>
<li class="">Remplir les pistes inutilisées avec des parties de la prochaine ronde logique lorsque des séances compactes comptent davantage qu’une nouvelle rangée par ronde.</li>
<li class="">Voir précisément les parties à l’origine des valeurs <strong>Maximum de répétitions sur une piste</strong>, <strong>Total des répétitions</strong> et <strong>Répétitions consécutives</strong>.</li>
<li class="">Ajouter une partie ponctuelle à un tournoi à la ronde précis pour qu’elle fasse partie de ses classements et pointages.</li>
<li class="">Importer des affectations CSV pour des parties existantes sans recréer la structure de compétition.</li>
<li class="">Enregistrer l’arrangement actuel comme modèle d’horaire du club ou appliquer volontairement un modèle correspondant.</li>
<li class="">Partir d’un horaire canonique validé, puis optimiser les changements propres à l’événement sans déplacer les parties verrouillées.</li>
</ul>
<p>Ces règles sont propres au curling. Le répartiteur distingue les parties de tournoi à la ronde, ponctuelles et de tableau. Il comprend les rondes logiques qui peuvent s’étendre sur plusieurs séances, tient compte des ressources sélectionnées et des placements fixes, et mesure les retours des équipes sur une même piste, y compris les séances consécutives. Il place les parties ponctuelles après les parties de tournoi à la ronde des équipes concernées, puis les parties de tableau déverrouillées après les parties de tournoi à la ronde et ponctuelles. Une partie verrouillée échappe à ces règles de placement.</p>
<p>Lorsqu’un événement contient un tournoi à la ronde complet et aucun verrou incompatible, <strong>Répartir</strong> vérifie notre catalogue avant toute recherche dans le navigateur. Plusieurs itérations répètent la disposition correspondante du catalogue comme point de départ. Un modèle du club correspondant est présenté séparément et appliqué seulement si le responsable de l’horaire le choisit. Lorsque des verrous, parties ponctuelles, étapes multiples ou placements manuels changent le problème, l’optimiseur du navigateur travaille à partir de l’horaire réel.</p>
<p><img decoding="async" loading="lazy" alt="Une partie verrouillée et une partie ponctuelle dans l’éditeur d’événement Curling IO v3" src="https://curling.io/fr/assets/images/locked-and-extra-games-1480948d28ae5b65d9dac7a7347ed3b5.png" width="1280" height="720" class="img_amcT"></p>
<p><em>Le verrou bleu indique un placement que la répartition automatique doit conserver. La file contient une deuxième rencontre ponctuelle entre A et B, prête à placer.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="see-the-draw-scheduling-workflow">Voir le parcours de planification des séances<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#see-the-draw-scheduling-workflow" class="hash-link" aria-label="Lien direct vers Voir le parcours de planification des séances" title="Lien direct vers Voir le parcours de planification des séances" translate="no">​</a></h2>
<p>Ce tutoriel montre comment configurer, répartir, modifier, inspecter, optimiser, enregistrer et réutiliser un horaire de séances d’événement.</p>
<div class="text--center videoWrapper"><iframe width="100%" src="https://www.youtube.com/embed/wigR-bzj004?si=8tE3gk-KWvb7ZpKT" title="Créer et gérer un horaire de séances de curling dans Curling IO" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="for-drawmasters-how-we-built-the-schedule-catalog">Pour les responsables d’horaires : comment nous avons construit le catalogue<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#for-drawmasters-how-we-built-the-schedule-catalog" class="hash-link" aria-label="Lien direct vers Pour les responsables d’horaires : comment nous avons construit le catalogue" title="Lien direct vers Pour les responsables d’horaires : comment nous avons construit le catalogue" translate="no">​</a></h2>
<p><em>La suite devient technique. Elle explique la notation des horaires, les algorithmes testés, les solveurs exacts et la distinction entre les meilleurs horaires trouvés et les optimums prouvés. Vous êtes prévenus!</em></p>
<p>Le système de la version 3 a deux parties de calcul. Une application Rust hors ligne cherche et valide des horaires de base et prouve parfois leur optimalité. Un optimiseur JavaScript traite les changements propres à l’événement dans le navigateur : parties verrouillées ou ponctuelles et étapes de compétition partageant les mêmes ressources.</p>
<p>Le travail hors ligne a produit un catalogue versionné couvrant les 736 combinaisons de 2 à 24 équipes et de 1 à 16 pistes, selon les deux modes pris en charge de regroupement dans les séances. Curling IO v3 charge directement ce catalogue pour répartir un horaire d’événement.</p>
<p>Le catalogue contient actuellement 449 dispositions : 419 prouvées optimales et 30 meilleurs résultats trouvés jusqu’ici. Cela comprend 448 exigences distinctes dans l’enveloppe pratique et un cas historique hors norme conservé pour compatibilité.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="scheduling-objective">Objectif de planification<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#scheduling-objective" class="hash-link" aria-label="Lien direct vers Objectif de planification" title="Lien direct vers Objectif de planification" translate="no">​</a></h3>
<p>Nous évaluons l’équité des pistes comme un tuple ordonné :</p>
<ol>
<li class=""><strong>Maximum de retours :</strong> le plus grand nombre de visites supplémentaires d’une équipe sur une piste.</li>
<li class=""><strong>Total des retours :</strong> chaque visite de chaque équipe après sa première sur une piste.</li>
<li class=""><strong>Retours consécutifs :</strong> les cas où une équipe joue sur la même piste dans des séances voisines.</li>
</ol>
<p>Plus bas est meilleur. Nous comparons le premier nombre, puis le deuxième, puis le troisième. Le temps d’exécution ne compense jamais un horaire moins bon.</p>
<p>Pour huit équipes et quatre pistes, <code>(1, 24, 0)</code> signifie qu’aucune équipe ne visite une piste plus de deux fois, qu’il y a 24 retours au total et qu’aucune ne revient sur la même piste dans des séances consécutives.</p>
<p>Ces 24 retours sont inévitables. Huit équipes font 56 visites équipe-piste en sept séances, pour seulement 32 combinaisons uniques. Même un horaire parfaitement équilibré a au moins <code>56 - 32 = 24</code> retours. Atteindre cette borne inférieure avec au plus une visite supplémentaire et aucun retour consécutif prouve qu’il n’y a rien à améliorer selon nos règles de notation.</p>
<p><img decoding="async" loading="lazy" alt="L’inspecteur d’équité montrant les retours d’une équipe sur une piste" src="https://curling.io/fr/assets/images/repeat-highlights-31d5b6c8e316e492979c7d54f8b5f7cb.png" width="1280" height="720" class="img_amcT"></p>
<p><em>Les statistiques sont inspectables dans l’éditeur. En sélectionner une met en évidence les parties et équipes qui l’ont produite.</em></p>
<p>Nous avons choisi un tuple ordonné plutôt qu’une note pondérée, car les poids cachent les compromis. Éviter la sixième visite d’une équipe sur la piste A vaut-il trois nouveaux retours consécutifs ailleurs? Une note scalaire ne répond qu’après l’invention d’un taux de conversion. Le tuple rend les priorités explicites.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="how-good-is-ai-for-draw-schedule-generation">L’IA est-elle bonne pour générer des horaires de séances?<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#how-good-is-ai-for-draw-schedule-generation" class="hash-link" aria-label="Lien direct vers L’IA est-elle bonne pour générer des horaires de séances?" title="Lien direct vers L’IA est-elle bonne pour générer des horaires de séances?" translate="no">​</a></h3>
<p>Nous avons testé si les modèles IA généralistes actuels pouvaient produire ces horaires à partir d’une demande normale de responsable d’horaire. Le test portait sur un appel direct unique au modèle, pas sur un agent ayant accès à notre validateur ou à une boucle de correction.</p>
<p>La formulation a demandé du travail. Les premières demandes, pourtant tout à fait naturelles, retournaient souvent des affrontements manquants ou en double, surtout avec un nombre impair d’équipes. Nous les avons affinées pour approcher les précisions qu’un responsable donnerait dans une courte conversation. La version finale indiquait le nombre de parties attendu, demandait un seul tableau Markdown et demandait au modèle de vérifier son travail. Elle ne fournissait ni liste des affrontements, ni nombre minimal de séances, ni tuple d’équité, horaire en cache, méthode du cercle ou autre algorithme.</p>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Lire la traduction de la demande exacte pour 8 équipes et 4 pistes</summary><div><div class="collapsibleContent_ZBPi"><p>Crée un horaire de séances de curling pour un tournoi à la ronde simple avec 8 équipes et 4 pistes. Nomme les équipes de 1 à 8 et les pistes de A à D.</p><p>L’horaire doit contenir exactement 28 parties, une par paire d’équipes unique. Une équipe ne peut pas jouer plus d’une fois dans une séance.</p><p>Essaie de répartir les parties de chaque équipe entre les pistes, surtout en évitant la même piste dans des séances consécutives. Des affrontements corrects comptent plus qu’un équilibre parfait des pistes.</p><p>Avant de répondre, compte les parties et vérifie les affrontements manquants ou en double. Retourne un seul tableau Markdown complet, pas un tableau par séance. Utilise une rangée par séance et des colonnes nommées Draw, Sheet A à Sheet D. Écris chaque partie comme <code>1 vs 2</code>, laisse les cellules de pistes inutilisées vides et ajoute une colonne Bye au besoin.</p></div></div></details>
<p>Seuls le nombre d’équipes, le nombre de pistes, les libellés et le nombre attendu de parties changeaient pour les autres cas de test.</p>
<p>Nous avons envoyé cette demande figée à OpenAI et Anthropic par leurs API directes. Nous avons ajouté Gemini dans un passage distinct par OpenRouter, fixé sur Google AI Studio sans repli de fournisseur. Chaque modèle a reçu une fois les mêmes cinq cas : 8 équipes sur 4 pistes, 8 sur 2, 9 sur 4, 11 sur 5 et 14 sur 7. Chaque requête partait d’un contexte neuf d’un seul message, utilisait le raisonnement par défaut du fournisseur et avait une limite stricte de cinq minutes. Nous avons conservé toutes les réponses, y compris les dépassements de délai.</p>
<p>La disposition familière à 8 équipes et 4 pistes est publiée depuis des années et pourrait apparaître dans les données d’entraînement. Nous l’avons gardée comme cas d’étalonnage, pas comme preuve que le modèle avait trouvé la réponse lui-même. Nous avons aussi exclu de ce tableau les essais informels dans les applications de clavardage grand public, qui peuvent ajouter des instructions cachées, du routage de modèles et des réglages de raisonnement.</p>
<p>Les résultats ont été analysés et vérifiés indépendamment. Le formatage sans conséquence pouvait être normalisé, mais l’évaluateur ne pouvait pas ajouter ou retirer une partie, changer un affrontement ou améliorer une affectation de piste. Un horaire devait contenir chaque affrontement exactement une fois, limiter chaque équipe à une partie par séance et passer le même calcul d’équité que Drawmaster. Les horaires non valides ne recevaient aucune note d’équité.</p>
<table><thead><tr><th>API et modèle</th><th style="text-align:right">Résultats valides</th><th style="text-align:right">Temps de réponse médian</th><th style="text-align:right">Jetons de sortie</th><th style="text-align:right">Coût calculé</th></tr></thead><tbody><tr><td>OpenAI <code>gpt-5.6-sol</code></td><td style="text-align:right">5/5</td><td style="text-align:right">50,7 secondes</td><td style="text-align:right">21 079</td><td style="text-align:right">0,64 $</td></tr><tr><td>Anthropic <code>claude-opus-5</code></td><td style="text-align:right">5/5</td><td style="text-align:right">108,3 secondes</td><td style="text-align:right">59 070</td><td style="text-align:right">1,48 $</td></tr><tr><td>Gemini <code>google/gemini-3-flash-preview</code></td><td style="text-align:right">2/5</td><td style="text-align:right">3,7 secondes</td><td style="text-align:right">3 338</td><td style="text-align:right">0,01 $</td></tr></tbody></table>
<p>Les jetons de raisonnement sont inclus dans les totaux de sortie, sans être comptés une deuxième fois. Un passage sur cinq cas constitue une base utile, mais pas un classement statistiquement robuste des modèles.</p>
<p>OpenAI et Claude ont retourné un horaire valide au nombre minimal de séances pour chaque cas. OpenAI a égalé ou dépassé Claude dans les cinq cas. L’équité des pistes donne toutefois un autre résultat :</p>
<p>Gemini a terminé chaque demande en 5,1 secondes ou moins, mais trois de ses cinq horaires étaient non valides. Les réponses 8×4 et 14×7 répétaient des affrontements malgré leur affirmation que chaque paire avait été vérifiée. La réponse 9×4 répétait deux affrontements et plaçait une équipe deux fois dans sa dernière séance. Ses deux horaires valides utilisaient le nombre minimal de séances, mais aucun n’égalait l’équité du catalogue.</p>
<table><thead><tr><th>Cas</th><th>Catalogue actuel</th><th>Meilleur résultat IA</th><th>Comparaison</th></tr></thead><tbody><tr><td>8 équipes, 4 pistes</td><td><code>(1, 24, 0)</code></td><td><code>(2, 24, 0)</code></td><td>Curling IO est meilleur</td></tr><tr><td>8 équipes, 2 pistes</td><td><code>(3, 40, 0)</code></td><td><code>(3, 40, 0)</code></td><td>Égalité, optimum prouvé</td></tr><tr><td>9 équipes, 4 pistes</td><td><code>(1, 36, 0)</code></td><td><code>(2, 36, 0)</code></td><td>Curling IO est meilleur</td></tr><tr><td>11 équipes, 5 pistes</td><td><code>(1, 55, 0)</code></td><td><code>(2, 55, 0)</code></td><td>Curling IO est meilleur</td></tr><tr><td>14 équipes, 7 pistes</td><td><code>(1, 84, 0)</code></td><td><code>(2, 84, 0)</code></td><td>Curling IO est meilleur</td></tr></tbody></table>
<p>Notre recherche Rust simple à départ neutre a égalé le meilleur résultat d’OpenAI dans les cinq cas, avec quelques secondes de travail local combiné par cas. Le processus hors ligne beaucoup plus lourd a ensuite produit le catalogue actuel, meilleur dans quatre cas et égal dans le cinquième.</p>
<p>L’IA reste utile comme générateur supplémentaire de candidats hors ligne. Elle peut trouver un horaire manqué par nos méthodes. Nous traitons simplement sa réponse comme tout candidat non fiable : l’analyser, valider chaque affrontement et conflit, calculer la note nous-mêmes et prouver l’optimalité lorsque possible. Pour les horaires personnalisés, l’optimiseur du navigateur travaille autour des verrous et parties supplémentaires dans une limite de 100 ms, plutôt que de demander à un modèle généraliste de reconstruire l’horaire à partir de texte.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="search-envelope">Enveloppe de recherche<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#search-envelope" class="hash-link" aria-label="Lien direct vers Enveloppe de recherche" title="Lien direct vers Enveloppe de recherche" translate="no">​</a></h3>
<p>Notre enveloppe pratique comprend :</p>
<ul>
<li class="">De 2 à 24 équipes.</li>
<li class="">De 1 à 16 pistes sélectionnées.</li>
<li class="">Un tournoi à la ronde complet.</li>
<li class="">Une itération.</li>
<li class="">Aucune partie verrouillée, ponctuelle ou de tableau.</li>
<li class="">Deux comportements lorsqu’une ronde logique s’étend sur plusieurs séances : commencer chaque ronde dans une nouvelle séance ou remplir les pistes vides avec des parties de la ronde suivante.</li>
</ul>
<p>Cela donne <code>23 × 16 × 2 = 736</code> clés de recherche.</p>
<p>Les deux réglages de regroupement ne produisent pas toujours des dispositions différentes. Si toutes les parties d’une ronde tiennent dans une séance, aucune piste vide ne peut accueillir la ronde suivante. Regrouper ces cas équivalents réduit l’enveloppe à 448 exigences de disposition distinctes. Le catalogue stocke 449 dispositions parce qu’il conserve aussi une forme historique hors enveloppe : 26 équipes sur 7 pistes.</p>
<p>Cette frontière n’était pas arbitraire. Nous avons analysé les données Curling IO. Parmi des milliers d’horaires générés d’une seule itération avec peu de pistes, 98 % avaient au plus 20 équipes et 8 pistes. Les cinq formes les plus courantes étaient 8×4, 12×6, 6×3, 10×5 et 9×4, soit 63 % de cette cohorte.</p>
<p>Nous avons étendu la couverture à 16 pistes, car les dispositions avec des pistes excédentaires sont peu coûteuses à stocker et utiles à prendre en charge. Nous avons ensuite porté la limite d’équipes à 24. Les cas historiques hors norme de 42 équipes ne justifiaient toujours pas d’étendre autant la cible interactive.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="research-harness">Banc de recherche<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#research-harness" class="hash-link" aria-label="Lien direct vers Banc de recherche" title="Lien direct vers Banc de recherche" translate="no">​</a></h3>
<p>L’implémentation CurlingSchedules d’origine avait déjà un générateur circulaire de tournoi à la ronde, un répartiteur glouton de pistes, des ajustements locaux et un petit cache de correspondances exactes. Nous avions aussi beaucoup expérimenté, notamment avec une branche de recuit simulé.</p>
<p>Le premier problème était la mesure. Des essais historiques utilisaient des poids différents, d’autres une ancienne définition du maximum de répétitions, et certains tests ne vérifiaient pas ce qu’annonçait leur nom. Les résultats de fonctions objectifs différentes n’étaient pas directement comparables.</p>
<p>Nous avons créé une application Rust comme banc de recherche avec :</p>
<ul>
<li class="">Un modèle d’horaire de référence et un validateur indépendant.</li>
<li class="">Des cas de test et des graines déterministes.</li>
<li class="">Des stratégies de recherche interchangeables.</li>
<li class="">Des comparaisons appariées à budgets égaux.</li>
<li class="">Des rapports texte, JSON et CSV.</li>
<li class="">Une pondération selon les fréquences de production des données Curling IO.</li>
<li class="">Des points de contrôle atomiques permettant de reprendre les longues recherches.</li>
<li class="">Un catalogue versionné où chaque disposition porte sa méthode, sa graine, son effort, son temps d’exécution, sa borne inférieure et son statut de preuve.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Le parcours de recherche et de validation qui ajoute un horaire au catalogue canonique" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjU2MCIgdmlld0JveD0iMCAwIDEyMDAgNTYwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzY3JpcHRpb24iPgogIDx0aXRsZSBpZD0idGl0bGUiPkhvdyBhIGN1cmxpbmcgc2NoZWR1bGUgZW50ZXJzIHRoZSBjYW5vbmljYWwgY2F0YWxvZzwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2NyaXB0aW9uIj5HZW5lcmF0ZWQgc2NoZWR1bGVzIHBhc3MgdGhyb3VnaCBoZXVyaXN0aWMgc2VhcmNoIGFuZCBleGFjdCBtZXRob2RzLCB0aGVuIGluZGVwZW5kZW50IHZhbGlkYXRpb24sIGJlZm9yZSB0aGV5IGVudGVyIHRoZSB2ZXJzaW9uZWQgY2F0YWxvZyB1c2VkIGJ5IEN1cmxpbmcgSU8uPC9kZXNjPgogIDxkZWZzPgogICAgPGZpbHRlciBpZD0ic2hhZG93IiB4PSItMjAlIiB5PSItMjAlIiB3aWR0aD0iMTQwJSIgaGVpZ2h0PSIxNDAlIj4KICAgICAgPGZlRHJvcFNoYWRvdyBkeD0iMCIgZHk9IjIiIHN0ZERldmlhdGlvbj0iMyIgZmxvb2QtY29sb3I9IiMwZjE3MmEiIGZsb29kLW9wYWNpdHk9Ii4xMiIvPgogICAgPC9maWx0ZXI+CiAgPC9kZWZzPgoKICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI1NjAiIHJ4PSIyMCIgZmlsbD0iI2Y4ZmFmYyIvPgogIDx0ZXh0IHg9IjUyIiB5PSI2MiIgZmlsbD0iIzBmMTcyYSIgZm9udC1mYW1pbHk9IkludGVyLCB1aS1zYW5zLXNlcmlmLCBzeXN0ZW0tdWksIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzAiIGZvbnQtd2VpZ2h0PSI3MDAiPkhvdyBhIHNjaGVkdWxlIGVudGVycyB0aGUgY2F0YWxvZzwvdGV4dD4KICA8dGV4dCB4PSI1MiIgeT0iOTQiIGZpbGw9IiM0NzU1NjkiIGZvbnQtZmFtaWx5PSJJbnRlciwgdWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE3Ij5TZWFyY2ggYW5kIHByb29mIHVzZSB0aGUgc2FtZSBzY2hlZHVsZSBtb2RlbCwgc2NvcmluZyB0dXBsZSwgYW5kIHZhbGlkYXRvci48L3RleHQ+CgogIDxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzY0NzQ4YiIgc3Ryb2tlLXdpZHRoPSIzIj4KICAgIDxwYXRoIGQ9Ik0yNzQgMjM0aDI0Ii8+CiAgICA8cGF0aCBkPSJNNTQ2IDIzNGgyNCIvPgogICAgPHBhdGggZD0iTTgxOCAyMzRoMjQiLz4KICAgIDxwYXRoIGQ9Ik05NzQgMzE4djE4Ii8+CiAgPC9nPgogIDxnIGZpbGw9IiM2NDc0OGIiPgogICAgPHBhdGggZD0iTTI5OCAyMjhsMTAgNi0xMCA2eiIvPgogICAgPHBhdGggZD0iTTU3MCAyMjhsMTAgNi0xMCA2eiIvPgogICAgPHBhdGggZD0iTTg0MiAyMjhsMTAgNi0xMCA2eiIvPgogICAgPHBhdGggZD0iTTk2OCAzMzZoMTJsLTYgMTB6Ii8+CiAgPC9nPgoKICA8ZyBmaWx0ZXI9InVybCgjc2hhZG93KSIgZm9udC1mYW1pbHk9IkludGVyLCB1aS1zYW5zLXNlcmlmLCBzeXN0ZW0tdWksIHNhbnMtc2VyaWYiPgogICAgPGc+CiAgICAgIDxyZWN0IHg9IjQyIiB5PSIxNTAiIHdpZHRoPSIyMzIiIGhlaWdodD0iMTY4IiByeD0iMTQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2NiZDVlMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxjaXJjbGUgY3g9IjczIiBjeT0iMTgxIiByPSIxNCIgZmlsbD0iI2RiZWFmZSIvPgogICAgICA8dGV4dCB4PSI3MyIgeT0iMTg3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMWQ0ZWQ4IiBmb250LXNpemU9IjE2IiBmb250LXdlaWdodD0iNzAwIj4xPC90ZXh0PgogICAgICA8dGV4dCB4PSI5NiIgeT0iMTg4IiBmaWxsPSIjMGYxNzJhIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIj5Jbml0aWFsPC90ZXh0PgogICAgICA8dGV4dCB4PSI2OCIgeT0iMjI5IiBmaWxsPSIjNDc1NTY5IiBmb250LXNpemU9IjE2Ij5DaXJjbGUgcGFpcmluZ3M8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjY4IiB5PSIyNTMiIGZpbGw9IiM0NzU1NjkiIGZvbnQtc2l6ZT0iMTYiPkRldGVybWluaXN0aWMgc2VlZHM8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjY4IiB5PSIyNzciIGZpbGw9IiM0NzU1NjkiIGZvbnQtc2l6ZT0iMTYiPk5vIGNhY2hlZCBpbnB1dDwvdGV4dD4KICAgIDwvZz4KCiAgICA8Zz4KICAgICAgPHJlY3QgeD0iMzE0IiB5PSIxNTAiIHdpZHRoPSIyMzIiIGhlaWdodD0iMTY4IiByeD0iMTQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzkzYzVmZCIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxjaXJjbGUgY3g9IjM0NSIgY3k9IjE4MSIgcj0iMTQiIGZpbGw9IiNkYmVhZmUiLz4KICAgICAgPHRleHQgeD0iMzQ1IiB5PSIxODciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxZDRlZDgiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtd2VpZ2h0PSI3MDAiPjI8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjM2OCIgeT0iMTg4IiBmaWxsPSIjMGYxNzJhIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIj5IZXVyaXN0aWM8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjM0MCIgeT0iMjI5IiBmaWxsPSIjNDc1NTY5IiBmb250LXNpemU9IjE2Ij5UYXJnZXRlZCBsb2NhbCBzZWFyY2g8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjM0MCIgeT0iMjUzIiBmaWxsPSIjNDc1NTY5IiBmb250LXNpemU9IjE2Ij5SZWNvbnN0cnVjdGlvbjwvdGV4dD4KICAgICAgPHRleHQgeD0iMzQwIiB5PSIyNzciIGZpbGw9IiM0NzU1NjkiIGZvbnQtc2l6ZT0iMTYiPkluZGVwZW5kZW50IHBvcHVsYXRpb25zPC90ZXh0PgogICAgPC9nPgoKICAgIDxnPgogICAgICA8cmVjdCB4PSI1ODYiIHk9IjE1MCIgd2lkdGg9IjIzMiIgaGVpZ2h0PSIxNjgiIHJ4PSIxNCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjZmJiZjI0IiBzdHJva2Utd2lkdGg9IjIiLz4KICAgICAgPGNpcmNsZSBjeD0iNjE3IiBjeT0iMTgxIiByPSIxNCIgZmlsbD0iI2ZlZjNjNyIvPgogICAgICA8dGV4dCB4PSI2MTciIHk9IjE4NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2I0NTMwOSIgZm9udC1zaXplPSIxNiIgZm9udC13ZWlnaHQ9IjcwMCI+MzwvdGV4dD4KICAgICAgPHRleHQgeD0iNjQwIiB5PSIxODgiIGZpbGw9IiMwZjE3MmEiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiPkV4YWN0PC90ZXh0PgogICAgICA8dGV4dCB4PSI2MTIiIHk9IjIyOSIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNiI+Q29uc3RyYWludCBzb2x2aW5nPC90ZXh0PgogICAgICA8dGV4dCB4PSI2MTIiIHk9IjI1MyIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNiI+QnJhbmNoLWFuZC1ib3VuZDwvdGV4dD4KICAgICAgPHRleHQgeD0iNjEyIiB5PSIyNzciIGZpbGw9IiM0NzU1NjkiIGZvbnQtc2l6ZT0iMTYiPkNvbWJpbmF0b3JpYWwgZGVzaWduczwvdGV4dD4KICAgIDwvZz4KCiAgICA8Zz4KICAgICAgPHJlY3QgeD0iODU4IiB5PSIxNTAiIHdpZHRoPSIyMzIiIGhlaWdodD0iMTY4IiByeD0iMTQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2NiZDVlMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxjaXJjbGUgY3g9Ijg4OSIgY3k9IjE4MSIgcj0iMTQiIGZpbGw9IiNlMmU4ZjAiLz4KICAgICAgPHRleHQgeD0iODg5IiB5PSIxODciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMzMzQxNTUiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtd2VpZ2h0PSI3MDAiPjQ8L3RleHQ+CiAgICAgIDx0ZXh0IHg9IjkxMiIgeT0iMTg4IiBmaWxsPSIjMGYxNzJhIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIj5WYWxpZGF0aW9uPC90ZXh0PgogICAgICA8dGV4dCB4PSI4ODQiIHk9IjIyOSIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNSI+R2FtZXMsIGRyYXdzLCBhbmQgc2hlZXRzPC90ZXh0PgogICAgICA8dGV4dCB4PSI4ODQiIHk9IjI1MyIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNSI+RmFpcm5lc3MgYW5kIHByb29mIHN0YXR1czwvdGV4dD4KICAgIDwvZz4KCiAgICA8Zz4KICAgICAgPHJlY3QgeD0iNzIwIiB5PSIzNTIiIHdpZHRoPSIzNzAiIGhlaWdodD0iMTM0IiByeD0iMTQiIGZpbGw9IiNlZmY2ZmYiIHN0cm9rZT0iIzI1NjNlYiIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICAgIDxjaXJjbGUgY3g9Ijc1MSIgY3k9IjM4MyIgcj0iMTQiIGZpbGw9IiMyNTYzZWIiLz4KICAgICAgPHRleHQgeD0iNzUxIiB5PSIzODkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmYiIGZvbnQtc2l6ZT0iMTYiIGZvbnQtd2VpZ2h0PSI3MDAiPjU8L3RleHQ+CiAgICAgIDx0ZXh0IHg9Ijc3NCIgeT0iMzkwIiBmaWxsPSIjMGYxNzJhIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIj5DYXRhbG9nPC90ZXh0PgogICAgICA8dGV4dCB4PSI3NDYiIHk9IjQyOSIgZmlsbD0iIzQ3NTU2OSIgZm9udC1zaXplPSIxNiI+VmFsaWRhdGVkIGxheW91dHM8L3RleHQ+CiAgICAgIDx0ZXh0IHg9Ijc0NiIgeT0iNDUzIiBmaWxsPSIjNDc1NTY5IiBmb250LXNpemU9IjE2Ij5CdW5kbGVkIGludG8gQ3VybGluZyBJTyB2MzwvdGV4dD4KICAgIDwvZz4KICA8L2c+Cgo8L3N2Zz4K" width="1200" height="560" class="img_amcT"></p>
<p><em>Les horaires générés passent par la recherche heuristique, les méthodes exactes et un validateur indépendant avant d’entrer au catalogue versionné.</em></p>
<p>Les dispositions en cache servaient de cibles de comparaison, jamais d’entrées de recherche. Chaque algorithme devait produire son résultat à partir des mêmes conditions initiales générées.</p>
<p>Rust convenait bien à ce travail pour plusieurs des raisons de notre choix du langage côté serveur de Curling IO v3. L’exécution native permettait d’évaluer beaucoup de candidats. Le système de types rendait difficile le mélange d’un horaire partiel non valide avec un résultat censé être valide. Le même banc pouvait effectuer des comparaisons limitées ou reprendre de longues recherches exactes. C’était un petit projet de recherche appelé à changer constamment, mais l’exactitude comptait toujours.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="evaluated-algorithms">Algorithmes évalués<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#evaluated-algorithms" class="hash-link" aria-label="Lien direct vers Algorithmes évalués" title="Lien direct vers Algorithmes évalués" translate="no">​</a></h3>
<p>Aucun algorithme ne gagnait sur tous les cas. Le processus hors ligne final combine des méthodes aux coûts et forces différents.</p>
<table><thead><tr><th>Approche</th><th>Forces</th><th>Leçons</th></tr></thead><tbody><tr><td>Génération gloutonne</td><td>Produire rapidement une base déterministe valide</td><td>Les choix locaux de piste manquent de recul; multiplier les reconstructions aléatoires ne garantit pas un meilleur bassin</td></tr><tr><td>Échanges simples, doubles, triples et quadruples</td><td>Améliorations locales peu coûteuses</td><td>Deux ou trois déplacements peuvent sortir de certains optimums locaux, mais plus profond n’est pas automatiquement meilleur</td></tr><tr><td>Déplacements ciblés</td><td>Consacrer l’effort aux équipes et pistes qui bloquent la prochaine amélioration de l’équité</td><td>Réutiliser les registres de notation améliorait la qualité et la rapidité</td></tr><tr><td>Recherche taboue et parcours de plateaux</td><td>Passer par des états de même note</td><td>Une recherche limitée aux améliorations strictes se bloque trop tôt</td></tr><tr><td>Reconstruction gloutonne</td><td>Entrer dans une autre région de l’espace de recherche</td><td>Des redémarrages indépendants étaient plus utiles que le polissage répété d’un horaire</td></tr><tr><td>Groupes d’élite et populations</td><td>Conserver plusieurs candidats forts et différents</td><td>Plus de diversité ne produit pas automatiquement de meilleurs horaires</td></tr><tr><td>Recuit simulé</td><td>Améliorer occasionnellement un cas non résolu</td><td>Ce n’était pas une stratégie générale compétitive dans nos budgets mesurés</td></tr><tr><td>Destruction-reconstruction, croisement, rotations et déplacements de colonnes de pistes</td><td>Ajouter des voisinages précis à un portefeuille</td><td>La plupart dépendaient du cas, et plusieurs servaient seulement de témoins négatifs</td></tr><tr><td>Résolution de contraintes et séparation-évaluation</td><td>Prouver une cible atteignable ou impossible</td><td>Les méthodes exactes fonctionnent mieux après une bonne décomposition du problème</td></tr><tr><td>Constructions combinatoires</td><td>Résoudre directement des familles complètes</td><td>La bonne réponse est parfois une construction, pas davantage de recherche</td></tr></tbody></table>
<h4 class="anchor anchorTargetStickyNavbar_Kbpc" id="local-search">Recherche locale<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#local-search" class="hash-link" aria-label="Lien direct vers Recherche locale" title="Lien direct vers Recherche locale" translate="no">​</a></h4>
<p>L’optimiseur le plus simple essaie un échange légal de parties et le conserve seulement si le tuple s’améliore. Notre version de profondeur deux essaie un deuxième échange même si le premier est très mauvais. Elle garde les deux seulement si le résultat combiné bat l’horaire intact. Sur 20 départs neutres à huit équipes, la profondeur deux a battu la recherche à un échange 16 fois et atteint deux fois l’optimum connu.</p>
<p>La profondeur trois a encore amélioré les chances sans dominer la profondeur deux. Sur 1 000 départs appariés, elle a gagné 394 fois, fait égalité 310 fois et perdu 296 fois. La profondeur quatre a ensuite perdu plus souvent qu’elle n’a gagné contre la profondeur trois. De plus grands voisinages ouvraient des chemins, mais dépensaient aussi davantage du budget fixe à en explorer d’inutiles.</p>
<p>Le ciblage aidait plus régulièrement. Nous choisissions surtout les parties sources parmi les affectations équipe-piste qui bloquaient la prochaine réduction du maximum de répétitions. Nous avons ensuite ajouté une mémoire taboue pour parcourir des états d’équité égale encore inexplorés plutôt que de rebondir entre les mêmes arrangements.</p>
<p>Le cas classique 8×4 a tout de même montré la limite des déplacements locaux. Une vérification exacte a prouvé qu’un regroupement précis d’affrontements ne pouvait pas faire mieux que <code>(1,24,5)</code>. L’optimum global était <code>(1,24,0)</code>. Aucune permutation de pistes dans ce regroupement ne pouvait combler l’écart : il fallait regrouper les affrontements autrement entre les séances.</p>
<p>C’est une limite de la recherche locale. Certaines améliorations exigent de changer les parties qui partagent une séance, au-delà de leurs pistes.</p>
<h4 class="anchor anchorTargetStickyNavbar_Kbpc" id="simulated-annealing">Recuit simulé<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#simulated-annealing" class="hash-link" aria-label="Lien direct vers Recuit simulé" title="Lien direct vers Recuit simulé" translate="no">​</a></h4>
<p>Nous avons essayé le recuit simulé deux fois : dans l’ancienne implémentation Gleam, puis dans une expérience Rust volontairement plus petite. La grande version historique avait des courbes de température, du réchauffement, des redémarrages, des poids de déplacement adaptatifs, des cycles de pistes, des rotations de séances, des déplacements ciblés et plusieurs phases. Elle conservait le meilleur candidat. Retourner un état chaud dégradé n’expliquait donc pas sa performance.</p>
<p>Compte tenu de l’effort consacré à comprendre et à implémenter le recuit simulé, les résultats étaient décevants. La petite version Rust rendait la comparaison plus fiable. Elle restait compétitive sur 8×4, mais perdait sur l’ensemble plus large des cas. Le recuit apportait de la diversité au portefeuille pour une forme, sans preuve suffisante pour en faire une stratégie générale.</p>
<p>Nous avons conservé ce résultat négatif dans le dossier de recherche. Affiner davantage les températures était moins prioritaire que les approches améliorant plus de cas avec le même budget d’évaluation.</p>
<h4 class="anchor anchorTargetStickyNavbar_Kbpc" id="reconstruction-and-portfolios">Reconstruction et portefeuilles<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#reconstruction-and-portfolios" class="hash-link" aria-label="Lien direct vers Reconstruction et portefeuilles" title="Lien direct vers Reconstruction et portefeuilles" translate="no">​</a></h4>
<p>La reconstruction gloutonne conserve les séances et les règles de compétition, mais refait les affectations de pistes à partir d’un nouvel ordre déterminé par une graine. Cinq départs reconstruits indépendants ont battu cinq départs ordinaires avec le même budget total d’évaluation. Cela ne résolvait pas tous les cas, mais atteignait des bassins invisibles à la recherche par échanges seuls.</p>
<p>Notre portefeuille hors ligne a fini par conserver des processus de reconstruction et de population indépendants, avec une voie distincte qui exploitait le meilleur candidat validé trouvé jusqu’ici. Forcer tous les processus à repartir du meilleur résultat global réduisait la diversité sans aider. Le partage devenait utile seulement comme travail supplémentaire, sans remplacer la recherche privée de chaque processus.</p>
<p>Nous avons donc ajouté des points de contrôle pour l’état privé de chaque processus et le meilleur résultat global. Une longue recherche peut s’arrêter et reprendre en conservant les deux.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="exact-methods-and-constructions">Méthodes exactes et constructions<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#exact-methods-and-constructions" class="hash-link" aria-label="Lien direct vers Méthodes exactes et constructions" title="Lien direct vers Méthodes exactes et constructions" translate="no">​</a></h3>
<p>La recherche heuristique fournissait de bons candidats, mais prouver l’optimalité exigeait des méthodes exactes.</p>
<p>Nous les avons ajoutées par couches plutôt que de donner tout le problème à un seul solveur :</p>
<ol>
<li class=""><strong>Affectation de pistes à séances fixes :</strong> garde les affrontements et la composition des séances, puis résout seulement la piste de chaque partie.</li>
<li class=""><strong>Modèles de rondes logiques :</strong> peuvent répartir de nouveau les parties entre les séances d’une ronde en conservant ses frontières.</li>
<li class=""><strong>Recherche de factorisation compacte :</strong> choisit le regroupement d’affrontements et les pistes pour les petits horaires à séances pleines.</li>
<li class=""><strong>Requêtes de réparation bornées :</strong> demandent si une cible connue est atteignable dans un rayon donné d’une bonne solution courante.</li>
<li class=""><strong>Constructions combinatoires :</strong> génèrent directement des familles de plans connues, comme les plans de tournois équilibrés partitionnés.</li>
</ol>
<p>Le solveur de contraintes à séances fixes a amené plusieurs formes courantes à leur borne inférieure globale. Un passage exact ultérieur a prouvé l’optimalité de 64 des 74 dispositions à beaucoup de pistes encore non résolues, environ 86 %. La séparation-évaluation compacte a aussi prouvé la borne inférieure 10×5 à partir d’entrées générées, sans lire l’horaire en cache, et traité de petits horaires à nombre impair d’équipes avec des couplages presque parfaits.</p>
<p>Les résultats exacts ont aussi montré les limites de la formulation de recherche. Un tournoi à la ronde 16×8 est un plan de tournoi équilibré partitionné de côté huit. Une construction publiée de type starter-adder a produit directement la borne globale <code>(1,112,0)</code>. La recherche locale stagnait parce qu’un bon horaire heuristique exigeait de changer au moins 41 placements partie-piste dans sa composition de séances existante.</p>
<p>Pour 19 équipes sur 9 pistes, la réparation bornée ne pouvait pas atteindre <code>(1,171,17)</code> en changeant au plus 10 cellules. Nous avons retiré les variables redondantes et encodé directement la condition de borne inférieure : chaque équipe utilise chaque piste exactement deux fois. Z3 a trouvé <code>(1,171,0)</code> sur la structure cyclique d’affrontements existante. La solution différait des pistes de départ dans 151 des 171 cellules de parties, environ 88 %. Le rayon de réparation locale excluait les solutions pertinentes.</p>
<p>Nous avons ensuite exécuté une finition exacte sur l’ordre des rondes. Chaque ronde logique complète devient un bloc. La méthode cherche le meilleur chemin entre ces blocs sans changer les affrontements, les pistes ou les parties réunies dans une séance. Cela a amélioré 28 entrées du catalogue et prouvé l’optimalité de 23. Le passage refuse délibérément les horaires compacts dont les séances traversent les frontières de rondes plutôt que de deviner les regroupements.</p>
<p>Un court balayage de contraintes a produit 51 preuves supplémentaires. Quarante-neuf ont terminé en moins d’une seconde sur la machine de développement. Le plus grand écart de retours consécutifs restant concernait 17 équipes sur 8 pistes : il est passé de <code>(1,136,16)</code> à la borne globale <code>(1,136,0)</code> en environ 25 secondes. Les deux modes de regroupement à 13 équipes et 4 pistes restaient non résolus après une tentative d’une minute.</p>
<p>Enfin, nous avons généralisé l’énumérateur de rondes complètes pour permettre des pistes excédentaires. Il a épuisé six petits espaces de recherche, prouvé les six optimums et amélioré l’entrée à 6 équipes et 5 pistes de <code>(1,3,0)</code> à <code>(1,2,0)</code>. Ces preuves comptent, car la borne inférieure générique peu coûteuse n’est pas atteignable pour toutes les formes.</p>
<p>Dix-sept dispositions étaient encore marquées comme meilleurs résultats trouvés. La parité des degrés du graphe et une borne de transition sur une seule piste en ont prouvé 12 sans changer les horaires. Des constructions cycliques par différences d’arêtes ont ensuite résolu les formes à 9 et 13 équipes sur deux pistes. Des recherches complètes de coloration d’arêtes, d’appariement de séances et d’ordre des séances ont résolu celles de 6 et 7 équipes sur deux pistes.</p>
<p>Le dernier cas était 9 équipes sur 8 pistes. Les horaires complets avaient trop de symétries. Nous avons donc séparé le problème en plus petits problèmes exacts. Un modèle pseudo-booléen a d’abord affecté les 36 parties aux pistes. Une factorisation exacte a ensuite formé neuf séances légales, puis une recherche de chemin hamiltonien les a ordonnées sans retours consécutifs. Le résultat est passé de <code>(1,5,0)</code> à la borne globale <code>(1,4,0)</code>.</p>
<p>Nous avons ensuite étendu le catalogue à 24 équipes. Des plans publiés de tournois équilibrés partitionnés ont fourni des horaires exacts à 22 équipes et 11 pistes, puis à 24 équipes et 12 pistes. Séparer leurs deux moitiés équilibrées a aussi prouvé neuf formes à pistes excédentaires. L’ordre exact des séances et la construction existante de séparation pour les nombres impairs ont prouvé 14 autres dispositions. Les nouvelles formes restantes reçoivent toujours un horaire validé en cache, mais gardent <strong>Optimiser</strong> disponible pendant leur étude hors ligne.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="coverage-and-proof-status">Couverture et statut de preuve<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#coverage-and-proof-status" class="hash-link" aria-label="Lien direct vers Couverture et statut de preuve" title="Lien direct vers Couverture et statut de preuve" translate="no">​</a></h3>
<p>La révision 23 du catalogue résout toutes les formes d’équipes et de pistes dans l’enveloppe pratique de 2 à 24 équipes et de 1 à 16 pistes. Sur les 449 dispositions stockées, 419 sont prouvées optimales et 30 gardent des meilleurs résultats trouvés validés.</p>
<p>Parmi les 419 dispositions prouvées, 392 atteignent la borne inférieure globale générique. Les 27 autres exigent une preuve plus précise : 16 utilisent une borne de parité des degrés du graphe, une utilise une borne de transition sur une piste et dix utilisent une recherche exhaustive. La forme impaire à cinq équipes et deux pistes en est un exemple. L’énumération exacte a prouvé que quatre retours consécutifs ne pouvaient pas être éliminés. De petites formes à pistes excédentaires donnent d’autres exemples, car des parties partageant une séance doivent encore avoir des pistes distinctes.</p>
<p>Chaque recherche dans l’enveloppe définie a un résultat validé en cache. Les 30 meilleurs résultats trouvés restent dans la file d’amélioration. Les dispositions prouvées restent dans la suite de régression, mais ne reçoivent plus de budget de recherche hors ligne tant que l’enveloppe ou les règles de notation ne changent pas.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="offline-rust-and-browser-javascript">Rust hors ligne et JavaScript dans le navigateur<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#offline-rust-and-browser-javascript" class="hash-link" aria-label="Lien direct vers Rust hors ligne et JavaScript dans le navigateur" title="Lien direct vers Rust hors ligne et JavaScript dans le navigateur" translate="no">​</a></h3>
<p>Précalculer les dispositions ordinaires change le rôle de l’optimiseur du navigateur. Il sert aux horaires qui ne correspondent plus aux entrées canoniques.</p>
<p>Un événement réel peut avoir :</p>
<ul>
<li class="">Plusieurs groupes de tournoi à la ronde qui partagent des pistes.</li>
<li class="">Plusieurs itérations, où combiner des dispositions canoniques crée de nouveaux motifs de répétition.</li>
<li class="">Des parties verrouillées que <strong>Répartir</strong> et <strong>Optimiser</strong> ne doivent pas déplacer.</li>
<li class="">Des parties ponctuelles pour compléter des tournois à la ronde déséquilibrés.</li>
<li class="">Des parties de tableau qui doivent suivre leurs prédécesseurs.</li>
</ul>
<p>Ces combinaisons sont trop propres aux événements pour un cache global. Elles ont toujours besoin d’une optimisation immédiate et réversible dans l’éditeur.</p>
<p>L’optimiseur du navigateur fonctionne dans un Web Worker annulable avec une limite stricte actuelle de 100 ms. Il conserve toujours l’entrée et le meilleur résultat valide trouvé. Un temps plus long peut donc ne rien améliorer, mais ne peut pas retourner un horaire moins bon.</p>
<p>Son répartiteur actuel utilise trois grandes politiques :</p>
<ul>
<li class=""><strong>Ordinaire :</strong> un horaire déverrouillé qui demande une amélioration locale générale.</li>
<li class=""><strong>Réparation sous contraintes :</strong> des verrous ou placements fixes rendent la stabilité importante.</li>
<li class=""><strong>Compatible avec les tableaux :</strong> l’ordre des parties et les frontières fixes des séances comptent davantage qu’une reconstruction plus large.</li>
</ul>
<p>Il consacre 70 % du délai à la recherche de profondeur deux, puis le reste à la voie de réparation choisie. La voie compatible avec les tableaux garde la profondeur deux pendant tout le délai. Sur 700 cas de banc d’essai appariés, ce répartiteur à trois politiques a battu l’ancien sélecteur à cinq voies 22 fois, fait égalité 666 fois et perdu 12 fois.</p>
<p>Ce résultat apparié appuyait le répartiteur plus simple. Il retourne presque toujours une réponse égale ou meilleure, travaille hors du fil principal et choisit sa politique à partir des caractéristiques visibles de l’horaire.</p>
<p>Les implémentations navigateur et Rust partagent le vocabulaire, les attentes du validateur, le tuple d’équité, les cas de test et les preuves. Le budget Rust hors ligne est pratiquement sans contrainte : populations, solveurs exacts, processus parallèles et points de contrôle de longues recherches sont possibles. Celui du navigateur est strict, car une optimisation plus longue nuirait visiblement à la réactivité de l’éditeur.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="catalog-distribution-and-runtime-selection">Distribution du catalogue et sélection à l’exécution<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#catalog-distribution-and-runtime-selection" class="hash-link" aria-label="Lien direct vers Distribution du catalogue et sélection à l’exécution" title="Lien direct vers Distribution du catalogue et sélection à l’exécution" translate="no">​</a></h3>
<p>Drawmaster écrit un catalogue JSON versionné. Chaque entrée contient les équipes, séances et pistes indexées, ainsi que le tuple d’équité, la borne inférieure, la méthode de production, le statut de preuve, la commande et les éléments de validation indépendante.</p>
<p>Notre paquet partagé de planification Curling possède le catalogue, intégré au créateur d’horaires d’événement de Curling IO v3. L’éditeur gratuit <a href="https://curlingschedules.com/" target="_blank" rel="noopener noreferrer" class="">CurlingSchedules.com</a> utilise le même artefact, mais son interface et sa persistance dans le stockage local restent distinctes de Curling IO. Une recherche 8×4 trouve le même horaire canonique aux deux endroits.</p>
<p>L’ordre à l’exécution est le suivant :</p>
<ol>
<li class="">Chercher une disposition canonique exacte.</li>
<li class="">L’appliquer immédiatement lorsqu’elle correspond sans risque aux équipes et aux pistes choisies.</li>
<li class="">Répéter ou composer des dispositions pour les itérations et les groupes multiples.</li>
<li class="">Réparer autour des verrous, parties supplémentaires, parties de tableau et placements manuels.</li>
<li class="">Générer directement seulement si aucun point de départ du catalogue n’est utilisable.</li>
</ol>
<p>Une disposition exacte d’une seule itération marquée comme prouvée optimale désactive <strong>Optimiser</strong>. Le bouton n’a rien d’utile à faire. Le format du catalogue peut toujours contenir un meilleur résultat trouvé si l’enveloppe s’élargit avant la preuve de son optimum. Dans ce cas, <strong>Optimiser</strong> reste disponible. Les horaires répétés, composés ou réparés restent aussi optimisables, car la preuve d’une seule itération ne couvre plus le résultat combiné.</p>
<p>Les modèles du club restent distincts. Le responsable peut appliquer volontairement un arrangement local récurrent plutôt que le choix global par défaut. La version 3 avertit avant que ce modèle déplace des parties déjà placées. Les modèles expriment la préférence d’un club. Le catalogue canonique exprime le meilleur résultat général que nous pouvons valider pour une forme numérique.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="findings">Constats<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#findings" class="hash-link" aria-label="Lien direct vers Constats" title="Lien direct vers Constats" translate="no">​</a></h3>
<p>La recherche a produit ces conclusions de travail :</p>
<ul>
<li class="">Une couverture complète validée primait sur davantage de recherche pour une disposition déjà bien comprise.</li>
<li class="">Aucune heuristique ne dominait toutes les formes d’équipes et de pistes. Leur variance dépassait nos attentes.</li>
<li class="">Une plus grande profondeur ne garantissait pas un meilleur résultat avec un budget fixe.</li>
<li class="">Parcourir des plateaux de même note comptait, sans pouvoir réparer une mauvaise factorisation d’affrontements.</li>
<li class="">La reconstruction et les portefeuilles indépendants trouvaient différents bassins plus sûrement qu’une longue marche locale.</li>
<li class="">Le recuit simulé n’était pas assez performant pour devenir une stratégie générale.</li>
<li class="">Les solveurs exacts devenaient pratiques après une bonne séparation du problème en couches.</li>
<li class="">Les plans combinatoires publiés donnaient des solutions directes aux familles d’horaires applicables.</li>
<li class="">Le statut de preuve doit accompagner chaque réponse en cache.</li>
<li class="">Le meilleur optimiseur de production est souvent une recherche en cache suivie d’une petite réparation propre au domaine.</li>
</ul>
<p>Le catalogue actuel couvre l’enveloppe cible de la version 3, et chaque disposition stockée est prouvée optimale. La recherche restante porte sur la réparation propre à l’événement autour des verrous, parties ponctuelles, parties de tableau et ressources partagées, ainsi que sur les futures extensions de l’enveloppe.</p>
<p>Curling IO v3 associe chaque disposition abstraite du catalogue aux vraies équipes, ressources, séances et parties de l’événement. Le responsable peut ensuite appliquer des modèles du club, importer des affectations, ajouter des parties ponctuelles et modifier manuellement l’horaire en préservant les placements qu’il choisit de verrouiller.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="glossary">Glossaire<a href="https://curling.io/fr/blog/optimizing-curling-draw-schedules#glossary" class="hash-link" aria-label="Lien direct vers Glossaire" title="Lien direct vers Glossaire" translate="no">​</a></h2>
<ul>
<li class=""><strong>Tuple d’équité :</strong> la note ordonnée <code>(Max, Total, Back-to-back)</code> qui compare les horaires. Plus bas est meilleur, et les nombres sont comparés de gauche à droite.</li>
<li class=""><strong>Heuristique :</strong> méthode conçue pour trouver rapidement un bon horaire sans prouver qu’il est le meilleur possible.</li>
<li class=""><strong>Recherche locale :</strong> amélioration d’un horaire par des changements voisins de son arrangement actuel.</li>
<li class=""><strong>Profondeur d’échange :</strong> nombre de déplacements évalués ensemble avant de les conserver ou non. Une recherche de profondeur deux peut garder deux déplacements dont le résultat combiné améliore l’horaire initial.</li>
<li class=""><strong>Plateau :</strong> groupe d’horaires différents avec la même note.</li>
<li class=""><strong>Parcours de plateau :</strong> passage par des horaires de même note pour chercher une position permettant une amélioration.</li>
<li class=""><strong>Recherche taboue :</strong> conservation d’une courte mémoire des déplacements ou horaires récents pour éviter de les répéter en boucle.</li>
<li class=""><strong>Recuit simulé :</strong> recherche qui accepte parfois un déplacement moins bon pour sortir d’un optimum local, généralement avec une tolérance décroissante au fil du temps.</li>
<li class=""><strong>Bassin :</strong> région de l’espace de recherche où les déplacements voisins tendent vers le même optimum local.</li>
<li class=""><strong>Portefeuille :</strong> plusieurs stratégies ou processus de recherche indépendants, dont on conserve le meilleur résultat validé.</li>
<li class=""><strong>Méthode exacte ou solveur :</strong> méthode capable d’établir si une cible est atteignable ou de prouver qu’aucun meilleur horaire valide n’existe.</li>
<li class=""><strong>Séparation-évaluation :</strong> méthode exacte qui cesse d’explorer une branche lorsque sa borne mathématique prouve qu’elle ne peut pas battre le meilleur résultat connu.</li>
<li class=""><strong>Borne inférieure :</strong> note qu’aucun horaire valide ne peut battre. Atteindre une borne applicable prouve l’optimalité.</li>
<li class=""><strong>Prouvé optimal :</strong> horaire validé pour lequel aucun meilleur tuple d’équité ne peut exister.</li>
<li class=""><strong>Meilleur résultat trouvé :</strong> meilleur horaire validé trouvé jusqu’ici, sans preuve qu’il n’en existe pas de meilleur.</li>
<li class=""><strong>Point de contrôle :</strong> état de recherche enregistré qui permet de reprendre un long processus sans repartir de zéro.</li>
</ul>]]></content:encoded>
            <category>v3</category>
            <category>Gestion des compétitions</category>
            <category>Architecture</category>
            <category>Rust</category>
            <category>JavaScript</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Louer votre glace avec Curling IO]]></title>
            <link>https://curling.io/fr/blog/rentals-built-into-the-calendar</link>
            <guid>https://curling.io/fr/blog/rentals-built-into-the-calendar</guid>
            <pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Curling IO v3 réunit les horaires de location, la disponibilité publique, les widgets intégrés au site du club, les commandes, la capacité, les listes d’attente et les ressources groupées dans un même calendrier.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Les clubs de curling louent de la glace de pratique, des salons, des salles de réunion et d’autres ressources qu’ils définissent. Une location exige un produit et un prix, mais aussi un horaire, une ou plusieurs ressources, une disponibilité publique et une réservation enregistrée après la finalisation de la commande.</p>
<p>Dans Curling IO v3, un administrateur définit l’horaire de location et les ressources dans l’administration. Les plages disponibles apparaissent au calendrier public. Les curleurs peuvent en ajouter une au panier et finaliser leur commande. Cet article explique les options de location à venir et leur intégration au calendrier.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-changes-from-v2">Ce qui change par rapport à la version 2<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#what-changes-from-v2" class="hash-link" aria-label="Lien direct vers Ce qui change par rapport à la version 2" title="Lien direct vers Ce qui change par rapport à la version 2" translate="no">​</a></h2>
<p>Curling IO v2 permet déjà aux clubs de louer des lieux. La version 3 améliore grandement ce parcours et change la configuration et la gestion des locations :</p>
<ul>
<li class=""><strong>Administration des locations :</strong> créez et modifiez les locations dans <strong>Produits &gt; Locations</strong>, avec les horaires, ressources, prix et options de réservation réunis dans l’administration.</li>
<li class=""><strong>Gestion des ressources :</strong> les ressources remplacent les lieux de la version 2. Une ressource peut être une piste, un salon, une salle de réunion ou tout autre élément défini par l’organisation. Une section de paramètres dédiée gère les ressources utilisées par les horaires de location, les réservations manuelles, les fermetures et le calendrier.</li>
<li class=""><strong>Capacité et listes d’attente :</strong> une location peut accepter une réservation exclusive ou plusieurs réservations par plage. La capacité et les listes d’attente concernent précisément la ressource, la date et l’heure réservées.</li>
<li class=""><strong>Ressources groupées :</strong> plusieurs ressources peuvent former une plage. Une réservation peut réserver deux pistes ou l’ensemble des pistes.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="see-the-rental-workflow">Voir le parcours de location<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#see-the-rental-workflow" class="hash-link" aria-label="Lien direct vers Voir le parcours de location" title="Lien direct vers Voir le parcours de location" translate="no">​</a></h2>
<p>Ce tutoriel crée et réserve une location exclusive, configure une location avec capacité, puis termine avec une location groupée qui réserve deux pistes ensemble.</p>
<div class="text--center videoWrapper"><iframe width="100%" src="https://www.youtube.com/embed/IzBPT6nmIOo" title="Gérer les locations dans Curling IO" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="how-the-rental-schedule-works">Le fonctionnement de l’horaire de location<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#how-the-rental-schedule-works" class="hash-link" aria-label="Lien direct vers Le fonctionnement de l’horaire de location" title="Lien direct vers Le fonctionnement de l’horaire de location" translate="no">​</a></h2>
<p>Une location commence par une ou plusieurs ressources, une date, au moins une heure de début et une durée. Elle peut avoir lieu une fois ou se répéter chaque jour, semaine, mois ou année. Un administrateur peut définir une date de fin et exclure des dates sans créer un produit distinct pour chaque plage.</p>
<p>Par exemple, un club peut offrir deux heures de glace de pratique sur les pistes A et B à 16 h chaque jour pendant une semaine. Chaque piste et date forment une plage réservable distincte. La location garde un prix et un ensemble de règles communs.</p>
<p><img decoding="async" loading="lazy" alt="De la glace de pratique d’équipe configurée comme location quotidienne sur les pistes A et B." src="https://curling.io/fr/assets/images/team-practice-availability-en-e001589791aa7c07d434e1e37d86ed5b.png" width="1520" height="1308" class="img_amcT"></p>
<p>La location définit l’offre complète. Une plage est une ressource, une date et une heure de début qu’un curleur peut réserver. Si les pistes exigent des prix ou des règles différents, le club crée des locations distinctes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="how-rentals-appear-on-the-calendar">Les locations au calendrier<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#how-rentals-appear-on-the-calendar" class="hash-link" aria-label="Lien direct vers Les locations au calendrier" title="Lien direct vers Les locations au calendrier" translate="no">​</a></h2>
<p>Les locations disponibles apparaissent avec les parties et les autres activités au calendrier public. Les curleurs peuvent filtrer le calendrier pour n’afficher que les locations. Le club peut partager directement cette vue filtrée pour promouvoir la glace de pratique ou les réservations de salles.</p>
<p>Sur ordinateur, le calendrier offre des vues par jour, semaine, mois et liste. Sur téléphone, il devient une liste quotidienne simplifiée, avec des commandes de date et une carte par événement. Pour la date actuelle, cette vue omet les événements déjà terminés.</p>
<p>Sélectionner une location disponible affiche sa date, son heure, sa durée, son prix et le choix de ressource. Si plusieurs pistes équivalentes sont offertes séparément, le curleur peut en choisir une disponible avant d’ajouter la plage au panier.</p>
<p><img decoding="async" loading="lazy" alt="Les détails d’une réservation de glace de pratique d’équipe avec la piste A sélectionnée." src="https://curling.io/fr/assets/images/team-practice-booking-en-d3b27e29809b6f325aafa15aa1ecca1d.png" width="1104" height="628" class="img_amcT"></p>
<p>Ajouter une plage au panier ne la réserve pas. La disponibilité est vérifiée de nouveau à la finalisation de la commande. La ressource n’est attribuée qu’une fois la commande finalisée. Les paniers abandonnés ne changent pas la disponibilité publique.</p>
<p>Une fois la commande finalisée, la personne qui a réservé voit l’état <strong>Réservé</strong>. Les autres curleurs voient <strong>Réservé</strong>, sans le nom de la location ni les détails de la commande. La piste B peut rester disponible à la même heure si seule la piste A a été réservée.</p>
<p><img decoding="async" loading="lazy" alt="Une réservation terminée sur la piste A tandis que la même location reste disponible sur la piste B." src="https://curling.io/fr/assets/images/team-practice-calendar-booked-en-473a0be0ee8b6f4d7efdfa4acb671b9b.png" width="2048" height="1536" class="img_amcT"></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="embed-rentals-on-your-club-website">Intégrer les locations au site de votre club<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#embed-rentals-on-your-club-website" class="hash-link" aria-label="Lien direct vers Intégrer les locations au site de votre club" title="Lien direct vers Intégrer les locations au site de votre club" translate="no">​</a></h2>
<p>Le club n’a pas à envoyer les visiteurs vers un calendrier distinct pour trouver de la glace disponible. Le widget de location place une liste ciblée des plages réservables sur sept jours directement sur le site existant du club. Les visiteurs comparent les dates, heures, ressources et prix sans quitter la page.</p>
<p><img decoding="async" loading="lazy" alt="Le widget de location Curling IO intégré à la page de glace de pratique d’un club." src="https://curling.io/fr/assets/images/embedded-rental-widget-desktop-e7090e871e9128265cbbfece61316e9f.png" width="2880" height="2000" class="img_amcT"></p>
<p>Le widget peut montrer toutes les locations publiques ou seulement certains produits et ressources. Un club peut consacrer une page à la glace de pratique, une autre aux salons, ou afficher seulement les pistes d’un programme. La liste reste liée au même horaire et aux mêmes conflits de ressources que le calendrier complet. Le club ne maintient donc pas la disponibilité à deux endroits.</p>
<p>Sélectionner <strong>Réserver</strong> mène au panier et au parcours de commande Curling IO habituel du club. C’est là que Curling IO confirme de nouveau la disponibilité et gère la connexion, les participants, les questions, les renonciations, les extras, les rabais, le paiement et la réservation finale. La liste intégrée sert à trouver une plage. Elle ne remplace pas le parcours de commande par une version simplifiée qui omet les règles du club.</p>
<p>La mise en page s’adapte à l’espace disponible sur la page hôte, y compris aux colonnes étroites et aux téléphones. Elle reprend aussi les couleurs Curling IO du club et protège ses commandes contre les styles contradictoires du site.</p>
<p><img decoding="async" loading="lazy" alt="Le widget de location intégré et adapté au site d’un club sur téléphone." src="https://curling.io/fr/assets/images/embedded-rental-widget-mobile-c469f29c24e55195eb527e59c5f29ec4.png" width="860" height="1840" class="img_amcT"></p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="exclusive-rentals-and-rentals-with-capacity">Locations exclusives et locations avec capacité<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#exclusive-rentals-and-rentals-with-capacity" class="hash-link" aria-label="Lien direct vers Locations exclusives et locations avec capacité" title="Lien direct vers Locations exclusives et locations avec capacité" translate="no">​</a></h2>
<p>Certaines locations sont exclusives. Une réservation prend la piste ou la salle complète pour cette plage. C’est la configuration naturelle pour de la glace de pratique.</p>
<p>D’autres acceptent plusieurs réservations pour la même ressource et heure. Une pratique libre pourrait utiliser la piste C à 19 h avec une capacité de quatre réservations. Après la finalisation d’une commande, la plage reste disponible pour trois autres réservations.</p>
<p><img decoding="async" loading="lazy" alt="Une pratique libre avec une capacité de quatre réservations." src="https://curling.io/fr/assets/images/drop-in-capacity-en-ea92aa8043bb5c0781194ffd7f2fc335.png" width="1520" height="436" class="img_amcT"></p>
<p>Le club peut aussi limiter chaque compte ou participant à un achat par plage. Si la plage est complète, une liste d’attente facultative recueille l’intérêt pour cette ressource, date et heure précises.</p>
<p>Lorsqu’un administrateur approuve une entrée de liste d’attente, le compte concerné peut réserver une place même si la capacité est atteinte. Cette exception est réservée à ce compte et n’augmente pas la capacité offerte aux autres.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="group-several-sheets-into-one-rental">Grouper plusieurs pistes dans une location<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#group-several-sheets-into-one-rental" class="hash-link" aria-label="Lien direct vers Grouper plusieurs pistes dans une location" title="Lien direct vers Grouper plusieurs pistes dans une location" translate="no">​</a></h2>
<p>La ressource louée n’est pas toujours une seule piste. Un instructeur peut vouloir deux pistes voisines, ou une ligue de location peut vouloir l’ensemble des pistes pour une période.</p>
<p>Un administrateur peut grouper les ressources sélectionnées pour qu’une seule réservation les réserve ensemble. La vue quotidienne montre les ressources groupées voisines comme un bloc qui s’étend sur leurs colonnes. Sélectionner le bloc montre toutes les ressources. La commande crée une réservation pour le groupe.</p>
<p><img decoding="async" loading="lazy" alt="Une pratique sur l’ensemble des pistes comme location groupée couvrant les pistes A et B." src="https://curling.io/fr/assets/images/full-rink-calendar-grouped-en-efc44ea4d4e7e284ae96ed6457555042.png" width="2048" height="794" class="img_amcT"></p>
<p>Un conflit sur une ressource rend toute la plage groupée indisponible. Si une partie planifiée, une fermeture, une réservation manuelle ou une réservation achetée utilise la piste A, le calendrier n’offre pas une version partielle de la location sur la piste B.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="calendar-actions-for-administrators">Actions du calendrier pour les administrateurs<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#calendar-actions-for-administrators" class="hash-link" aria-label="Lien direct vers Actions du calendrier pour les administrateurs" title="Lien direct vers Actions du calendrier pour les administrateurs" translate="no">​</a></h2>
<p>Le calendrier va au-delà d’une liste publique de locations. Les administrateurs peuvent ajouter des réservations manuelles et des fermetures, modifier un horaire de location et déplacer une réservation existante à partir de l’événement lui-même.</p>
<p>Ces éléments ont des fonctions distinctes :</p>
<ul>
<li class="">Une location est un produit public avec un prix, un horaire, un panier, une commande et un paiement.</li>
<li class="">Une réservation manuelle retient une ressource.</li>
<li class="">Une fermeture indique qu’une ressource est indisponible.</li>
<li class="">Une réservation achetée est la ressource et l’heure enregistrées sur une commande.</li>
</ul>
<p>Le calendrier combine ces éléments. Retirer une fermeture ou une réservation manuelle rend la plage de location sous-jacente de nouveau disponible sans que l’administrateur doive recréer son horaire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="other-product-features-available-to-rentals">Les autres fonctions de produit offertes aux locations<a href="https://curling.io/fr/blog/rentals-built-into-the-calendar#other-product-features-available-to-rentals" class="hash-link" aria-label="Lien direct vers Les autres fonctions de produit offertes aux locations" title="Lien direct vers Les autres fonctions de produit offertes aux locations" translate="no">​</a></h2>
<p>Les locations utilisent les mêmes outils commerciaux de base que les autres produits Curling IO. Un club peut exiger un participant, ajouter des questions d’inscription, offrir des extras, appliquer des rabais, attribuer des taxes et des paramètres comptables, puis utiliser le parcours normal de paiement et de remboursement.</p>
<p>Par exemple, la réservation d’une salle peut demander les besoins d’aménagement, celle d’une pratique peut exiger un participant, et une location de toutes les pistes peut inclure un extra connexe.</p>
<p>Les locations seront une fonction Premium de Curling IO v3. L’horaire de location sera la source commune pour calculer la disponibilité au calendrier, valider la commande et créer la réservation enregistrée avec la commande finalisée.</p>]]></content:encoded>
            <category>v3</category>
            <category>Locations</category>
            <category>Calendrier</category>
            <category>Gestion de club</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Des modèles de renonciation gérés pour les associations membres]]></title>
            <link>https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations</link>
            <guid>https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations</guid>
            <pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Curling IO v3 aidera les associations membres à diffuser des renonciations verrouillées, suivre les versions et voir quels clubs membres recueillent des réponses.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Les associations membres ont parfois besoin de plus qu’une renonciation recommandée. Une police d’assurance collective peut exiger que chaque club membre présente le même texte approuvé à ses participants.</p>
<p>Envoyer un document Word à chaque club diffuse le texte sans en garder le contrôle. Un club peut modifier sa copie, manquer la mise à jour suivante ou continuer de recueillir des réponses à une ancienne version. L’association risque ensuite de ne pas pouvoir déterminer de façon fiable le document qu’un participant a réellement vu.</p>
<p>Curling IO v3 reliera ces éléments. Une association membre peut publier une renonciation verrouillée, la rendre disponible à ses clubs membres, la mettre à jour au même endroit et voir quels clubs ont des participants qui y répondent.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="see-the-managed-waiver-workflow">Voir le parcours des renonciations gérées<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#see-the-managed-waiver-workflow" class="hash-link" aria-label="Lien direct vers Voir le parcours des renonciations gérées" title="Lien direct vers Voir le parcours des renonciations gérées" translate="no">​</a></h2>
<p>Ce tutoriel montre le parcours complet : créer des modèles gérés et personnalisables, publier une nouvelle version, activer les renonciations de l’association dans un club membre et consulter les versions et les réponses.</p>
<div class="text--center videoWrapper"><iframe width="100%" src="https://www.youtube.com/embed/kkbquPdg9oM" title="Modèles de renonciation gérés pour les associations membres dans Curling IO v3" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe></div>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="from-group-insurance-policy-to-participant-response">De la police d’assurance collective à la réponse du participant<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#from-group-insurance-policy-to-participant-response" class="hash-link" aria-label="Lien direct vers De la police d’assurance collective à la réponse du participant" title="Lien direct vers De la police d’assurance collective à la réponse du participant" translate="no">​</a></h2>
<p>Supposons que la police d’assurance collective de l’association de curling de démonstration exige que chaque participant adulte accepte une renonciation approuvée relative à la responsabilité civile générale. Voici comment cela fonctionnerait dans Curling IO v3.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="1-the-association-publishes-a-managed-waiver-template">1. L’association publie un modèle de renonciation géré<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#1-the-association-publishes-a-managed-waiver-template" class="hash-link" aria-label="Lien direct vers 1. L’association publie un modèle de renonciation géré" title="Lien direct vers 1. L’association publie un modèle de renonciation géré" translate="no">​</a></h3>
<p>Un modèle géré est une renonciation contrôlée par l’association que les clubs membres peuvent utiliser sans la modifier. L’association le crée une seule fois et contrôle le nom, le contenu, le texte d’acceptation et la plage d’âge inclusive. Un âge minimum de 18 ans inclut les participants de 18 ans et plus.</p>
<p>La renonciation peut contenir des variables pour le nom du club membre, sa province et son association membre. Curling IO les remplace pour chaque club. L’association n’a donc pas à maintenir des copies distinctes de documents autrement identiques.</p>
<p><img decoding="async" loading="lazy" alt="Les modèles de renonciation gérés et personnalisables créés par une association membre dans Curling IO v3." src="https://curling.io/fr/assets/images/membership-association-templates-en-e285b7657b0feeb0d763008cad226225.png" width="1992" height="560" class="img_amcT"></p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="2-member-clubs-enable-the-association-waiver">2. Les clubs membres activent la renonciation de l’association<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#2-member-clubs-enable-the-association-waiver" class="hash-link" aria-label="Lien direct vers 2. Les clubs membres activent la renonciation de l’association" title="Lien direct vers 2. Les clubs membres activent la renonciation de l’association" translate="no">​</a></h3>
<p>Les renonciations gérées disponibles sont activées par défaut pour les nouveaux clubs membres. Lorsqu’une association ajoute ensuite une renonciation gérée, les clubs existants la voient désactivée dans leur liste <strong>Renonciations de l'association</strong>. Ils peuvent la consulter et l’activer lorsqu’ils sont prêts à recueillir des réponses.</p>
<p>Une fois activée, le club ne peut pas changer le document ou la plage d’âge contrôlés par l’association. Il peut désactiver la renonciation, mais ne peut pas en modifier discrètement le texte tout en continuant de la présenter comme celle de l’association.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="3-participants-respond-during-registration">3. Les participants répondent pendant l’inscription<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#3-participants-respond-during-registration" class="hash-link" aria-label="Lien direct vers 3. Les participants répondent pendant l’inscription" title="Lien direct vers 3. Les participants répondent pendant l’inscription" translate="no">​</a></h3>
<p>Lorsqu’une renonciation s’applique à un participant, Curling IO présente le document actuel de l’association pendant l’inscription. Le titulaire du compte l’accepte pour lui-même ou répond, lorsque nécessaire, comme tuteur légal du participant.</p>
<p>La réponse enregistrée contient le modèle géré, sa version et le document exact dont les variables ont été remplacées, tel qu’affiché à ce moment. Elle consigne aussi l’âge du participant, la personne qui a répondu et son lien de tutelle, le cas échéant.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="4-the-association-can-see-which-clubs-are-using-it">4. L’association voit quels clubs l’utilisent<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#4-the-association-can-see-which-clubs-are-using-it" class="hash-link" aria-label="Lien direct vers 4. L’association voit quels clubs l’utilisent" title="Lien direct vers 4. L’association voit quels clubs l’utilisent" translate="no">​</a></h3>
<p>Les rapports de l’association membre indiqueront quels clubs ont des réponses de participants liées à chaque renonciation gérée. C’est plus utile que de vérifier si une renonciation est activée. Un club peut l’activer sans inscrire personne, tandis qu’une réponse enregistrée prouve que le document géré a été présenté et a reçu une réponse.</p>
<p>Le rapport fournit la preuve que les clubs recueillent des réponses à la renonciation gérée par l’association. Il ne conclut pas automatiquement, sur le plan juridique, qu’un club a satisfait à toutes les conditions d’une police d’assurance. Ces polices peuvent imposer des exigences que Curling IO ne peut pas déduire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="update-every-club-without-replacing-history">Mettre à jour tous les clubs sans remplacer l’historique<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#update-every-club-without-replacing-history" class="hash-link" aria-label="Lien direct vers Mettre à jour tous les clubs sans remplacer l’historique" title="Lien direct vers Mettre à jour tous les clubs sans remplacer l’historique" translate="no">​</a></h2>
<p>Lorsque le texte approuvé ou les exigences d’âge changent, l’association publie une nouvelle version du modèle géré. Elle devient immédiatement le document présenté par chaque club membre qui utilise cette renonciation.</p>
<p>Les personnes qui n’ont pas répondu voient la dernière version. Les participants qui ont déjà répondu pendant la saison ne sont pas sollicités de nouveau simplement parce que l’association a publié une mise à jour.</p>
<p>L’ancienne version et ses réponses ne sont pas écrasées. Les clubs membres peuvent consulter l’historique des versions et comparer les changements, mais ne peuvent pas ramener une renonciation gérée à une ancienne version ni appliquer la leur. Chaque réponse historique montre toujours le document exact présenté à ce participant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="use-a-customizable-template-when-locking-is-not-required">Utiliser un modèle personnalisable lorsqu’un verrouillage n’est pas requis<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#use-a-customizable-template-when-locking-is-not-required" class="hash-link" aria-label="Lien direct vers Utiliser un modèle personnalisable lorsqu’un verrouillage n’est pas requis" title="Lien direct vers Utiliser un modèle personnalisable lorsqu’un verrouillage n’est pas requis" translate="no">​</a></h2>
<p>Un contrôle central n’est pas toujours nécessaire. Une association peut proposer un texte de départ utile que les clubs adaptent à leurs programmes, installations ou conseils juridiques locaux. Elle peut alors publier un modèle personnalisable. Voici les différences pratiques :</p>
<table><thead><tr><th></th><th>Modèle géré</th><th>Modèle personnalisable</th></tr></thead><tbody><tr><td>Texte contrôlé par</td><td>L’association membre</td><td>Le club membre</td></tr><tr><td>Mises à jour ultérieures de l’association</td><td>Appliquées à tous les clubs qui l’utilisent</td><td>Ne changent pas les renonciations enregistrées des clubs</td></tr><tr><td>Réponses liées au modèle de l’association</td><td>Oui</td><td>Non</td></tr><tr><td>Usage recommandé</td><td>Assurance collective ou politique d’association exigeant un texte uniforme</td><td>Texte de départ facultatif que les clubs peuvent adapter</td></tr></tbody></table>
<p>Choisir un modèle personnalisable préremplit un nouveau formulaire de renonciation. Le club peut modifier le contenu avant d’enregistrer, et la renonciation obtenue lui appartient. C’est pratique, mais la commodité et la conformité sont deux choses distinctes. Si l’assureur exige un texte inchangé dans tous les clubs, le modèle géré est conçu pour conserver ce contrôle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="more-than-a-waiver-upload-and-checkbox">Plus qu’un document téléversé et une case à cocher<a href="https://curling.io/fr/blog/v3-waiver-templates-for-membership-associations#more-than-a-waiver-upload-and-checkbox" class="hash-link" aria-label="Lien direct vers Plus qu’un document téléversé et une case à cocher" title="Lien direct vers Plus qu’un document téléversé et une case à cocher" translate="no">​</a></h2>
<p>Un simple champ de renonciation peut afficher un texte et recueillir un accord. Il ne donne pas à une association le contrôle de ce document dans des clubs indépendants, ne conserve pas chaque version et ne relie pas chaque réponse à sa source gérée.</p>
<p>Ce lien est l’élément important de la conception de la version 3. L’association publie l’exigence. Les clubs l’utilisent sans la copier. Les participants répondent au document actuel. Les dossiers obtenus indiquent la version présentée et l’endroit où elle a été utilisée.</p>
<p>Voici le plus récent article de notre série sur les fonctions à venir dans Curling IO v3. Nous continuerons de montrer les vrais parcours à mesure qu’ils prennent forme, y compris les changements pour les associations membres, les clubs membres et les personnes qui s’inscrivent.</p>]]></content:encoded>
            <category>v3</category>
            <category>Associations membres</category>
            <category>Renonciations</category>
            <category>Assurance collective</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Ce que nous avons trouvé utile dans NixOS]]></title>
            <link>https://curling.io/fr/blog/improving-recovery-with-nixos</link>
            <guid>https://curling.io/fr/blog/improving-recovery-with-nixos</guid>
            <pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cette note technique d’implémentation porte sur l’architecture d’infrastructure et de récupération de Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aux responsables de l’exploitation, et aborde NixOS, le provisionnement de serveurs physiques, les secrets, le déploiement, les sauvegardes et la validation de récupération plus en détail que nos articles habituels sur le produit.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cette note technique d’implémentation porte sur l’architecture d’infrastructure et de récupération de Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aux responsables de l’exploitation, et aborde NixOS, le provisionnement de serveurs physiques, les secrets, le déploiement, les sauvegardes et la validation de récupération plus en détail que nos articles habituels sur le produit.</p></div></div>
<p>Curling IO fonctionnait auparavant sur Debian configuré avec Ansible. Cette installation marchait. Elle installait les paquets, configurait Caddy et le pare-feu, créait les services systemd et préparait nos emplacements de déploiement bleu-vert.</p>
<p>Le problème venait du fait qu’un serveur fonctionnel et modifiable nous permettait de conserver une description incomplète. Ansible pouvait décrire le serveur, mais d’anciens fichiers restaient sous <code>/etc</code>. Des outils de compilation avaient été installés autrement. Les choix du fournisseur sur les disques et le RAID existaient en dehors du playbook. Un provisionnement pouvait réussir parce qu’une exécution antérieure avait déjà laissé le bon élément en place.</p>
<p>NixOS tolère beaucoup moins ces lacunes. Cette rigueur est parfois agaçante, mais constitue aussi son principal avantage. Elle nous oblige à décider qui est responsable d’un fichier, d’un service, d’un secret, d’un paquet ou d’un élément nécessaire à la récupération.</p>
<p>Le passage d’Ansible à NixOS nous a montré combien de validations et de nettoyages manquaient à l’ancienne approche. Il a exposé la différence entre un serveur qui continue de fonctionner et un serveur qu’on peut recréer à partir d’éléments déclarés.</p>
<p>Voici ce que nous avons trouvé utile en reconstruisant un serveur physique OVH à partir de disques vides, en restaurant son état et en rendant le processus assez sûr pour être répété.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-boundary-we-wanted">La frontière que nous voulions<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#the-boundary-we-wanted" class="hash-link" aria-label="Lien direct vers La frontière que nous voulions" title="Lien direct vers La frontière que nous voulions" translate="no">​</a></h2>
<p>Nous voulions répondre à une question simple : si le serveur disparaissait, pourrions-nous reconstruire Curling IO rapidement, sans état non documenté de l’ancien hôte et pratiquement sans perte de données clients?</p>
<p>Une récupération réussie devait :</p>
<ul>
<li class="">Recréer les disques en miroir et la disposition de démarrage EFI.</li>
<li class="">Activer la configuration exacte de l’hôte.</li>
<li class="">Rétablir sa capacité à déchiffrer ses secrets.</li>
<li class="">Restaurer un état Caddy avec des certificats valides.</li>
<li class="">Compiler et déployer la plateforme, le site public et l’application Operations depuis une seule révision Git figée.</li>
<li class="">Restaurer et valider SQLite depuis Litestream.</li>
<li class="">Vérifier le stockage des fichiers téléversés publics et privés.</li>
<li class="">Créer et vérifier un instantané chiffré de la base de données dans une deuxième région.</li>
<li class="">Réussir les vérifications locales, publiques, de stockage, de surveillance et du fournisseur.</li>
</ul>
<p>Cela dépasse le provisionnement du système d’exploitation. NixOS est responsable de l’hôte, mais les versions applicatives et l’état des données clients ont leurs propres parcours.</p>
<table><thead><tr><th>Élément</th><th>Responsable</th></tr></thead><tbody><tr><td>Disposition des disques et installation initiale</td><td>OVH BYOLinux et notre hook de démarrage de l’image de récupération</td></tr><tr><td>Paquets et services du système d’exploitation</td><td>Configuration NixOS</td></tr><tr><td>Configuration générée de l’hôte</td><td>Chemins versionnés du magasin Nix</td></tr><tr><td>Activation des secrets de l’hôte</td><td>Parcours de récupération SOPS</td></tr><tr><td>Compilations de la plateforme, du site public et d’Operations</td><td>Parcours de déploiement indépendants</td></tr><tr><td>Installation et activation des versions</td><td>Commandes de déploiement indépendantes côté serveur</td></tr><tr><td>Migrations de base de données</td><td>Parcours de déploiement de la plateforme</td></tr><tr><td>Surveillance de l’hôte et des applications</td><td>Application Operations déployée indépendamment</td></tr><tr><td>Base clients et fichiers téléversés</td><td>Parcours Litestream et Object Storage</td></tr></tbody></table>
<p>Cette séparation tient bien. Nous pouvons changer l’hôte sans déployer une application, déployer la plateforme sans déployer le site public et restaurer une base sans réinstaller le serveur.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="application-releases-stay-separate">Les versions applicatives restent distinctes<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#application-releases-stay-separate" class="hash-link" aria-label="Lien direct vers Les versions applicatives restent distinctes" title="Lien direct vers Les versions applicatives restent distinctes" translate="no">​</a></h2>
<p>NixOS est responsable de l’hôte, pas de la compilation des applications. Nous compilons la plateforme, le site public et Operations avant le déploiement, les emballons en versions vérifiées, puis les téléversons par SSH. La production n’a besoin ni de code source, ni d’identifiants de dépôt, ni de chaînes de compilation ou de caches de compilation.</p>
<p>L’empreinte logicielle de production est donc plus petite : moins de paquets et d’outils exécutables, aucun dépôt de sources ou identifiant de compilation et moins d’état de compilation modifiable. Cela réduit la surface d’attaque et le fardeau des correctifs, sans remplacer le cloisonnement des services ou les mises à jour rapides.</p>
<p>Les changements d’hôte et les versions applicatives restent indépendants. La récupération a aussi une frontière simple : compiler depuis une révision Git figée, vérifier chaque paquet avant et après son téléversement, puis l’activer par le même parcours de déploiement qu’en fonctionnement normal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="nix-has-two-different-host-deployment-paths">Nix a deux parcours distincts de déploiement de l’hôte<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#nix-has-two-different-host-deployment-paths" class="hash-link" aria-label="Lien direct vers Nix a deux parcours distincts de déploiement de l’hôte" title="Lien direct vers Nix a deux parcours distincts de déploiement de l’hôte" translate="no">​</a></h2>
<p>Le provisionnement habituel et la récupération sur un hôte de remplacement fonctionnent volontairement différemment.</p>
<p>Pour un changement normal de configuration, notre script <code>bin/provision production</code> envoie les sources d’infrastructure exactes et validées dans Git au serveur en fonctionnement. Celui-ci utilise Nix pour construire une nouvelle génération système immuable, la valider, y passer et garder la génération précédente pour un retour arrière.</p>
<p>La récupération sur un hôte vierge ne peut pas dépendre de ce parcours. Une image de récupération censée être complète ne devrait pas démarrer pour ensuite télécharger ou construire la majorité du système réel. Notre image BYOLinux contient donc :</p>
<ul>
<li class="">Un système d’amorçage minimal sans secrets, capable de démarrer et d’accepter SSH.</li>
<li class="">La fermeture NixOS de production complète, c’est-à-dire tous ses éléments et dépendances, comme contenu inactif du magasin.</li>
</ul>
<p>Après le premier démarrage, la commande de récupération active le système de production déjà intégré et ses secrets requis. Si l’image ne contient pas la fermeture nécessaire, la récupération s’arrête. Elle ne masque pas l’image incomplète en se rabattant sur une construction à distance.</p>
<p>L’image de récupération est donc un véritable artefact plutôt qu’un mince installateur. Les commits touchant seulement les applications peuvent la réutiliser, car sa dérivation dépend uniquement des sources d’infrastructure. Le code applicatif, la documentation et les fichiers de tâches n’imposent pas de reconstruire une image de plusieurs gigaoctets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="bare-metal-keeps-the-provider-in-the-design">Le serveur physique garde le fournisseur dans la conception<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#bare-metal-keeps-the-provider-in-the-design" class="hash-link" aria-label="Lien direct vers Le serveur physique garde le fournisseur dans la conception" title="Lien direct vers Le serveur physique garde le fournisseur dans la conception" translate="no">​</a></h2>
<p>NixOS n’élimine pas OVH du parcours de récupération.</p>
<p>Notre premier installateur créait des métadonnées RAID logiciel valides avec un noyau Linux plus récent. Le noyau NixOS installé et le noyau de secours d’OVH refusaient tous deux de les assembler. Mettre seulement NixOS à jour aurait fait fonctionner le système installé, mais aurait laissé l’environnement de secours du fournisseur incapable de lire les disques.</p>
<p>Nous avons changé la répartition des responsabilités. OVH BYOLinux crée maintenant les systèmes de fichiers ext4 en miroir selon la disposition prise en charge par le fournisseur. Notre image fournit le système de fichiers NixOS et un hook de démarrage qui étiquette les systèmes de fichiers et installe GRUB EFI au chemin de média amovible sur les deux disques.</p>
<p>Le hook BYOLinux du fournisseur fonctionne dans un chroot restreint. Lors des premiers essais, il n’avait ni l’interpréteur de commandes ni le <code>PATH</code> attendus. La réussite de la construction d’image ne disait rien de la capacité d’OVH à exécuter le hook. Il a fallu :</p>
<ul>
<li class="">Utiliser un chemin absolu du magasin Nix pour Bash.</li>
<li class="">Intégrer le hook directement plutôt que d’appeler un deuxième chemin du magasin absent de l’image.</li>
<li class="">Utiliser les commandes internes de l’interpréteur jusqu’à la disponibilité du <code>PATH</code> du système intégré.</li>
<li class="">Tester chaque chemin du magasin référencé contre l’image elle-même.</li>
<li class="">Rejouer le hook dans un chroot semblable à celui du fournisseur.</li>
</ul>
<p>C’était décevant. QCOW2 offre un format d’image standard, mais pas un contrat standard pour en faire une installation physique démarrable. Nous pensions que la frontière du fournisseur serait plus prévisible. OVH exécutait plutôt un hook qui lui était propre dans un chroot minimal, dont il fallait découvrir l’interpréteur, le <code>PATH</code> et les chemins du magasin par des tests. Le format était portable, pas le contrat d’installation.</p>
<p>La surveillance du fournisseur comptait aussi. Lors d’une première répétition, OVH a détecté l’interruption prévue et ouvert une intervention technique. Celle-ci a bloqué une demande d’API de changement de mode de démarrage. La vérification préalable désactive maintenant l’intervention automatique et le confirme avant toute réinstallation. Elle rétablit ensuite la politique choisie lorsque l’hôte récupéré a réussi ses vérifications.</p>
<p>Résoudre cette frontière avec le fournisseur a pris la majeure partie du temps d’ingénierie consacré au provisionnement de récupération. La réinstallation réussie a pris moins de huit minutes, mais y arriver a exigé des constructions d’images répétées, des hooks en échec, des démarrages de secours et des répétitions destructives.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="generated-configuration-needs-one-owner">La configuration générée exige un seul responsable<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#generated-configuration-needs-one-owner" class="hash-link" aria-label="Lien direct vers La configuration générée exige un seul responsable" title="Lien direct vers La configuration générée exige un seul responsable" translate="no">​</a></h2>
<p>Caddy a vite exposé le problème de la configuration modifiable.</p>
<p>L’ancien serveur avait des fragments générés copiés dans des répertoires modifiables. Après un changement de disposition, des fichiers périmés restaient et étaient encore chargés. La configuration était correcte dans le dépôt, mais erronée sur le serveur, qui contenait à la fois les anciennes et les nouvelles interprétations.</p>
<p>La configuration NixOS possède maintenant les fichiers Caddy générés sous forme de liens vers des chemins exacts du magasin Nix. Les chemins obsolètes sont supprimés explicitement, et la configuration assemblée est validée avant l’activation.</p>
<p>Cette leçon dépasse NixOS. Un déploiement Ansible peut imposer la même règle, mais doit le faire délibérément. Copier le dernier fichier souhaité ne suffit pas si un ancien fichier peut encore influer sur l’exécution.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="recovery-state-must-exist-before-the-wipe">L’état de récupération doit exister avant l’effacement<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#recovery-state-must-exist-before-the-wipe" class="hash-link" aria-label="Lien direct vers L’état de récupération doit exister avant l’effacement" title="Lien direct vers L’état de récupération doit exister avant l’effacement" translate="no">​</a></h2>
<p>Le serveur en panne n’est pas une source de récupération.</p>
<p>Avant de l’effacer, la vérification préalable contrôle :</p>
<ul>
<li class="">La révision d’infrastructure et l’image de récupération choisies.</li>
<li class="">Les éléments requis pour récupérer les secrets.</li>
<li class="">L’état actuel du RAID.</li>
<li class="">Une sauvegarde déchiffrable de l’état Caddy avec des certificats valides.</li>
<li class="">La réplique Litestream hors site et l’instantané chiffré dans un deuxième emplacement hors site.</li>
<li class="">Les frontières de stockage des fichiers téléversés publics et privés.</li>
<li class="">Les points d’accès publics et l’état OVH.</li>
</ul>
<p>L’état Caddy est devenu un élément explicite après notre constat que l’émission des certificats ne devrait pas se trouver sur le chemin critique de récupération. NixOS vérifie les changements d’état Caddy toutes les cinq minutes et stocke des copies chiffrées hors site. Des emplacements de rétention hebdomadaire offrent un filet de sécurité.</p>
<p>Pendant la récupération, Caddy reste arrêté le temps de préparer la plateforme, le site, la base, les fichiers téléversés et l’instantané chiffré. Il ne démarre que lorsque le serveur peut présenter un système complet avec les certificats restaurés. Le renouvellement normal en arrière-plan reprend ensuite.</p>
<p>SQLite suit un parcours distinct. Litestream réplique continuellement hors site. Une tâche quotidienne restaure et valide cette réplique avant de créer un instantané complet, chiffré côté client, dans un deuxième emplacement hors site. La restauration empêche qu’un ancien instantané de sinistre se réplique vers la réplique principale pendant son test.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="recovery-runs-from-a-frozen-revision">La récupération part d’une révision figée<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#recovery-runs-from-a-frozen-revision" class="hash-link" aria-label="Lien direct vers La récupération part d’une révision figée" title="Lien direct vers La récupération part d’une révision figée" translate="no">​</a></h2>
<p>Une longue récupération ne peut pas suivre en toute sécurité une branche qui change.</p>
<p>La commande complète crée une copie de travail temporaire au commit choisi. La publication d’image, l’installation par le fournisseur, l’activation de l’hôte, les trois compilations applicatives, la restauration de l’état et la validation finale partent de cette arborescence figée. De nouveaux commits dans la copie normale ne peuvent pas modifier une récupération en cours.</p>
<p>Le parcours possède aussi des points de reprise durables :</p>
<ol>
<li class="">Vérifier l’image et les éléments de récupération.</li>
<li class="">Demander à OVH d’installer l’image.</li>
<li class="">Activer la fermeture NixOS intégrée.</li>
<li class="">Construire localement les paquets applicatifs vérifiés.</li>
<li class="">Déployer la plateforme et le site public.</li>
<li class="">Restaurer et valider SQLite, puis vérifier le stockage des fichiers téléversés.</li>
<li class="">Démarrer Caddy.</li>
<li class="">Déployer Operations.</li>
<li class="">Exécuter les vérifications finales d’acceptation.</li>
</ol>
<p>Si le déploiement applicatif échoue après le démarrage de l’hôte, nous reprenons au déploiement applicatif. Nous n’effaçons pas les disques une nouvelle fois. Le basculement du DNS public est une autre opération distincte et confirmée, après les vérifications directes de l’hôte de remplacement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="updates-use-the-same-health-check">Les mises à jour utilisent la même vérification de santé<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#updates-use-the-same-health-check" class="hash-link" aria-label="Lien direct vers Les mises à jour utilisent la même vérification de santé" title="Lien direct vers Les mises à jour utilisent la même vérification de santé" translate="no">​</a></h2>
<p>NixOS cherche les mises à jour stables chaque nuit et garde la génération précédente pour un retour arrière. Une nouvelle génération n’est acceptée qu’après avoir réussi les mêmes vérifications de santé en lecture seule que la surveillance Operations.</p>
<p>Le provisionnement, les mises à jour automatiques, la récupération et la surveillance partagent donc une définition d’un serveur sain. Si les vérifications échouent, l’hôte retourne à la génération précédente.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="lessons-beyond-nixos">Les leçons au-delà de NixOS<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#lessons-beyond-nixos" class="hash-link" aria-label="Lien direct vers Les leçons au-delà de NixOS" title="Lien direct vers Les leçons au-delà de NixOS" translate="no">​</a></h2>
<p>La même rigueur permet d’aller assez loin avec Debian et Ansible :</p>
<ul>
<li class="">Provisionner depuis une révision exacte.</li>
<li class="">Définir explicitement les responsabilités et le nettoyage.</li>
<li class="">Fixer les versions des outils de récupération.</li>
<li class="">Protéger les commandes destructives.</li>
<li class="">Tester automatiquement tout le parcours sur un hôte de remplacement.</li>
</ul>
<p>Rien de cela n’exige NixOS. La différence est qu’Ansible permet de sauter ces étapes et de continuer. NixOS rend les désaccords avec le système déclaré beaucoup plus difficiles à ignorer. Nous aimons ces garde-fous techniques chez Curling IO.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="decision">Décision<a href="https://curling.io/fr/blog/improving-recovery-with-nixos#decision" class="hash-link" aria-label="Lien direct vers Décision" title="Lien direct vers Décision" translate="no">​</a></h2>
<p>Nous gardons NixOS.</p>
<p>Il ne compile pas nos applications, ne restaure pas les données clients, n’élimine pas le fournisseur et ne remplace pas les scripts de déploiement. Il donne à ces systèmes un hôte mieux défini et une frontière d’activation plus sûre.</p>
<p>Le plus utile n’est pas la reproductibilité comme propriété abstraite. C’est l’obligation de rendre explicites les hypothèses cachées :</p>
<ul>
<li class="">Quel système est responsable de ce fichier?</li>
<li class="">Quelle clé peut récupérer ce secret?</li>
<li class="">Quel artefact identifie cet hôte?</li>
<li class="">Qu’est-ce qui survit à la perte du serveur?</li>
<li class="">Qu’est-ce qui prouve qu’une nouvelle génération est saine?</li>
<li class="">Où la récupération peut-elle reprendre après un échec?</li>
</ul>
<p>Ces questions ont amélioré tout le système de déploiement, y compris les parties sans Nix.</p>
<hr>
<p><em>Cet article est le neuvième de la série sur les fondations de Curling IO. Précédent : <a class="" href="https://curling.io/fr/blog/live-admin-without-javascript">un panneau d’administration en direct sans écrire de JavaScript</a>.</em></p>]]></content:encoded>
            <category>Fondations</category>
            <category>NixOS</category>
            <category>Déploiement</category>
            <category>Opérations</category>
        </item>
        <item>
            <title><![CDATA[Votre club, vos types de produits]]></title>
            <link>https://curling.io/fr/blog/your-club-your-product-types</link>
            <guid>https://curling.io/fr/blog/your-club-your-product-types</guid>
            <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Curling IO v3 permet aux organisations de créer des types de produits adaptés à leurs camps, ateliers, locations, collectes de fonds et autres activités.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Les clubs de curling vendent bien plus que des ligues et des bonspiels. Ils organisent des ateliers, louent de la glace, attribuent des casiers, tiennent des camps juniors, vendent des tables de banquet et recueillent des dons.</p>
<p>Aujourd’hui, chaque offre doit entrer dans un des types prédéfinis de Curling IO. Cela fonctionne, mais parfois seulement parce que le club choisit la catégorie la plus proche. Un camp d’été junior actif est stocké comme produit, avec ses dates d’août dans le nom. Une location de piste complète est stockée comme programme, car elle exige une date, une capacité et une liste d’attente. Un don est stocké comme frais.</p>
<p>Dans Curling IO v3, l’organisation décide des types dont elle a besoin. Un club peut créer Camps, Ateliers, Locations de glace, Locations de casiers, Collectes de fonds ou toute autre catégorie adaptée à ses activités, puis choisir ce que chaque type permet.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="start-with-what-your-club-calls-it">Commencez par les noms utilisés au club<a href="https://curling.io/fr/blog/your-club-your-product-types#start-with-what-your-club-calls-it" class="hash-link" aria-label="Lien direct vers Commencez par les noms utilisés au club" title="Lien direct vers Commencez par les noms utilisés au club" translate="no">​</a></h2>
<p>Supposons qu’un club nous dise :</p>
<blockquote>
<p>Nous offrons des ateliers d’initiation au curling et des camps juniors. Nous louons aussi de la glace de pratique, vendons des casiers et organisons un souper-bénéfice. Tout cela ne devrait pas se mélanger aux programmes ou aux produits.</p>
</blockquote>
<p>Dans la version 3, ce ne sera plus nécessaire. Le club peut créer un type pour chaque activité, les ordonner comme le personnel s’y attend et désactiver ceux qu’il n’utilise pas.</p>
<p><img decoding="async" loading="lazy" alt="Les paramètres des types de produits dans Curling IO v3" src="https://curling.io/fr/assets/images/product-types-0d7123539c738a0f2dfaa2f2935a0fe2.png" width="2266" height="1586" class="img_amcT"></p>
<p>Les nouvelles organisations reçoivent encore des types de départ familiers comme Ligues, Bonspiels, Programmes, Produits et Frais. Ce n’est pas un menu permanent imposé par Curling IO. Un administrateur peut les renommer, les réorganiser, les désactiver ou créer un autre type.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="these-categories-already-exist-in-the-real-world">Ces catégories existent déjà sur le terrain<a href="https://curling.io/fr/blog/your-club-your-product-types#these-categories-already-exist-in-the-real-world" class="hash-link" aria-label="Lien direct vers Ces catégories existent déjà sur le terrain" title="Lien direct vers Ces catégories existent déjà sur le terrain" translate="no">​</a></h2>
<p>Nous avons regardé les offres actives dans Curling IO plutôt que d’inventer des exemples. Voici quelques produits et services que les clubs et associations vendent actuellement :</p>
<table><thead><tr><th>Offre actuelle</th><th>Catégorie dans la version 2</th><th>Type naturel dans la version 3</th></tr></thead><tbody><tr><td>Camp d’été junior en août</td><td>Produit, avec les dates dans le nom</td><td>Camps</td></tr><tr><td>Location de piste complète de deux heures</td><td>Programme, avec quatre places disponibles et une liste d’attente</td><td>Locations de glace</td></tr><tr><td>Atelier de perfectionnement en soirée</td><td>Programme, avec une date, 20 places et une liste d’attente</td><td>Ateliers</td></tr><tr><td>Demi-casier pour la saison</td><td>Produit attribué à un participant</td><td>Locations de casiers</td></tr><tr><td>Table de huit à un souper-bénéfice</td><td>Produit</td><td>Collectes de fonds</td></tr><tr><td>Don supplémentaire de 100 $</td><td>Frais</td><td>Dons</td></tr></tbody></table>
<p>Ces choix actuels ne sont pas des erreurs. Ils montrent la limite d’un ensemble fixe de types. Les clubs utilisent déjà Curling IO pour ces activités, mais la structure des produits ne parle pas toujours leur langage.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="a-type-controls-more-than-its-label">Un type définit plus que son nom<a href="https://curling.io/fr/blog/your-club-your-product-types#a-type-controls-more-than-its-label" class="hash-link" aria-label="Lien direct vers Un type définit plus que son nom" title="Lien direct vers Un type définit plus que son nom" translate="no">​</a></h2>
<p>Un type de produit détermine les outils disponibles pour ses produits.</p>
<p>Un club qui crée un type « Ateliers » pourrait activer l’achat, l’inscription de participants, les dates, la capacité, les listes d’attente et les directeurs de produit. Chaque atelier peut ensuite avoir un prix, un formulaire d’inscription, des dates d’ouverture et de fermeture, une limite de participants et du personnel affecté.</p>
<p>Un type « Locations de glace » pourrait avoir besoin des achats, des dates et de l’inventaire, sans inscription de participants. Un type « Dons » pourrait avoir besoin des achats et de la comptabilité sans dates, équipes ou liste d’attente.</p>
<p>Le résultat se reflète dans le reste de Curling IO. Les types activés apparaissent dans la section <strong>Produits</strong>, dans l’ordre choisi par l’organisation. Chaque type peut avoir son nom et son résumé bilingues, son code de compte et l’adresse de sa liste publique.</p>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Que permet un type de produit?</summary><div><div class="collapsibleContent_ZBPi"><p>Curling IO v3 offre actuellement six capacités :</p><ul>
<li class=""><strong>Achetable :</strong> prix, taxes, rabais, ajouts, frais de retard et comptabilité.</li>
<li class=""><strong>Inscription :</strong> inscriptions de participants, questions personnalisées, règles d’admissibilité et dossiers d’inscription.</li>
<li class=""><strong>Dates :</strong> dates de début et de fin, plus dates d’ouverture et de fermeture des inscriptions.</li>
<li class=""><strong>Inventaire :</strong> limites de capacité ou de stock et listes d’attente facultatives.</li>
<li class=""><strong>Événement :</strong> équipes, horaires, étapes et résultats.</li>
<li class=""><strong>Directeurs :</strong> accès du personnel limité à des produits précis.</li>
</ul><p>Ces capacités sont des comportements pris en charge et intégrés à Curling IO. Créer un type de produit ne crée pas arbitrairement de nouvelles fonctions logicielles. Cela permet à une organisation de combiner les comportements existants dans une catégorie qui a du sens pour son personnel et ses clients.</p></div></div></details>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="familiar-defaults-without-the-fixed-boxes">Des types familiers, sans cases imposées<a href="https://curling.io/fr/blog/your-club-your-product-types#familiar-defaults-without-the-fixed-boxes" class="hash-link" aria-label="Lien direct vers Des types familiers, sans cases imposées" title="Lien direct vers Des types familiers, sans cases imposées" translate="no">​</a></h2>
<p>La plupart des clubs peuvent continuer d’utiliser Ligues, Bonspiels, Programmes, Produits et Frais. Aucun administrateur n’est obligé de concevoir un système de catégories avant d’ouvrir les inscriptions.</p>
<p>Mais le club qui organise des camps peut créer Camps. L’association qui donne des cours aux entraîneurs peut créer Cours. L’installation qui vend de la glace de pratique peut créer Locations de glace. La navigation administrative, les listes publiques et la configuration des produits peuvent refléter les activités que les gens connaissent déjà.</p>
<p>Les types de produits personnalisés arrivent dans Curling IO v3.</p>]]></content:encoded>
            <category>v3</category>
            <category>Gestion de club</category>
            <category>Produits</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Pourquoi votre club de curling ne devrait pas utiliser un CMS]]></title>
            <link>https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms</link>
            <guid>https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms</guid>
            <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Les CMS généralistes comme WordPress et Joomla ajoutent des risques de sécurité importants et un fardeau d’entretien aux clubs de curling. Voici ce que vous devez savoir.]]></description>
            <content:encoded><![CDATA[<p>Nous savons que plusieurs de nos clubs utilisent WordPress ou Joomla pour leur site web. Ces plateformes sont populaires pour de bonnes raisons : elles sont souples et ne manquent ni de tutoriels ni d’extensions. Mais cette popularité a un sérieux revers. Les systèmes de gestion de contenu généralistes, ou CMS, sont des cibles importantes, et les clubs gérés par des bénévoles n’ont souvent personne pour surveiller les correctifs de sécurité. Voici ce que vous devez savoir.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-security-problem-is-real">Le problème de sécurité est réel<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#the-security-problem-is-real" class="hash-link" aria-label="Lien direct vers Le problème de sécurité est réel" title="Lien direct vers Le problème de sécurité est réel" translate="no">​</a></h2>
<p>WordPress fait fonctionner environ 43 % des sites web. Cette part du marché en fait une cible immense pour les acteurs malveillants. Le <a href="https://sucuri.net/reports/2023-hacked-website-report/" target="_blank" rel="noopener noreferrer" class="">rapport 2023 de Sucuri sur les sites piratés</a> indique que WordPress représentait plus de 95 % des plateformes CMS piratées qu’ils ont réparées. Joomla et Drupal formaient la majeure partie du reste.</p>
<p>Le <a href="https://patchstack.com/whitepaper/state-of-wordpress-security-in-2024/" target="_blank" rel="noopener noreferrer" class="">rapport 2024 de Patchstack sur la sécurité de WordPress</a> a recensé 5 948 nouvelles vulnérabilités WordPress divulguées en un an. La grande majorité, plus de 97 %, se trouvaient dans les extensions et les thèmes plutôt que dans le cœur de WordPress. Le chiffre exact change chaque année, mais la tendance reste la même : les extensions et les thèmes concentrent la plupart des risques.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="supply-chain-attacks">Les attaques de la chaîne d’approvisionnement<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#supply-chain-attacks" class="hash-link" aria-label="Lien direct vers Les attaques de la chaîne d’approvisionnement" title="Lien direct vers Les attaques de la chaîne d’approvisionnement" translate="no">​</a></h2>
<p>Les bogues non corrigés ne sont pas le seul problème. En juin 2024, WordPress.org a révélé que <a href="https://www.wordfence.com/blog/2024/06/supply-chain-attack-on-wordpress-org-plugins-leads-to-5-maliciously-compromised-wordpress-plugins/" target="_blank" rel="noopener noreferrer" class="">plusieurs extensions du dépôt officiel avaient été compromises</a>. Les attaquants avaient obtenu l’accès à des comptes de développeurs et publié des mises à jour malveillantes d’extensions de confiance comptant des dizaines de milliers d’installations actives. Le code créait des comptes administratifs non autorisés et exfiltrait des données.</p>
<p>Une tendance croissante est encore plus inquiétante : des acteurs malveillants <strong>achètent des extensions légitimes et établies</strong> à leurs développeurs d’origine, puis injectent du code malveillant dans les mises à jour suivantes. Les utilisateurs qui ont activé les mises à jour automatiques, comme on le recommande généralement, reçoivent la version compromise sans indication du changement de propriétaire.</p>
<p>La <a href="https://blog.sucuri.net/2023/04/balada-injector-synopsis-of-a-massive-ongoing-wordpress-malware-campaign.html" target="_blank" rel="noopener noreferrer" class="">campagne Balada Injector</a>, documentée par Sucuri, exploite des vulnérabilités connues d’extensions depuis 2017. Selon leurs estimations, elle a compromis plus d’un million de sites WordPress en injectant du JavaScript malveillant qui redirige les visiteurs vers des sites frauduleux.</p>
<p>En août 2024, une <a href="https://patchstack.com/articles/critical-privilege-escalation-in-litespeed-cache-plugin-affecting-5-million-sites/" target="_blank" rel="noopener noreferrer" class="">vulnérabilité critique de l’extension LiteSpeed Cache</a>, utilisée par plus de cinq millions de sites, permettait de créer des comptes administratifs sur tout site utilisant la version vulnérable. Elle a été activement exploitée dans des attaques réelles.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="joomla-and-drupal-arent-immune">Joomla et Drupal ne sont pas à l’abri<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#joomla-and-drupal-arent-immune" class="hash-link" aria-label="Lien direct vers Joomla et Drupal ne sont pas à l’abri" title="Lien direct vers Joomla et Drupal ne sont pas à l’abri" translate="no">​</a></h2>
<p>Joomla a eu sa part de vulnérabilités critiques. En février 2024, une <a href="https://www.sonarsource.com/blog/joomla-multiple-xss-vulnerabilities/" target="_blank" rel="noopener noreferrer" class="">vulnérabilité XSS de gravité élevée dans le composant de filtrage du cœur de Joomla</a>, CVE-2024-21726, pouvait mener à l’exécution de code à distance. Un code d’exploitation de démonstration a été publié peu après sa divulgation. Une <a href="https://developer.joomla.org/security-centre/894-20230201-core-improper-access-check-in-webservice-endpoints.html" target="_blank" rel="noopener noreferrer" class="">vulnérabilité antérieure</a>, CVE-2023-23752, qui divulguait les identifiants de base de données, était encore exploitée massivement en 2024 parce que de nombreux sites restaient sans correctif.</p>
<p>Drupal a un meilleur bilan de sécurité grâce à un écosystème d’extensions mieux encadré et à une équipe de sécurité dédiée. Sa complexité fait toutefois que moins d’organisations le gardent entièrement à jour. Les tristement célèbres vulnérabilités Drupalgeddon de 2014 et 2018 étaient encore utilisées contre des sites non corrigés des années plus tard.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="automation-is-making-it-worse">L’automatisation aggrave la situation<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#automation-is-making-it-worse" class="hash-link" aria-label="Lien direct vers L’automatisation aggrave la situation" title="Lien direct vers L’automatisation aggrave la situation" translate="no">​</a></h2>
<p>Le trafic automatisé représente déjà <a href="https://www.imperva.com/resources/resource-library/reports/2024-bad-bot-report/" target="_blank" rel="noopener noreferrer" class="">environ la moitié du trafic Internet</a>, les robots malveillants comptant pour environ le tiers. Les outils IA ajoutent une couche : les attaquants peuvent s’en servir pour rechercher des vulnérabilités, générer du code d’exploitation et agir plus vite dès qu’une faille est publique.</p>
<p>Une <a href="https://arxiv.org/abs/2404.08144" target="_blank" rel="noopener noreferrer" class="">étude de l’Université de l’Illinois</a> a montré qu’un agent IA pouvait exploiter avec succès 87 % des vulnérabilités connues lorsqu’on lui fournissait leur description CVE. <a href="https://www.microsoft.com/en-us/security/blog/2024/02/14/staying-ahead-of-threat-actors-in-the-age-of-ai/" target="_blank" rel="noopener noreferrer" class="">Microsoft et OpenAI ont confirmé</a> que des acteurs affiliés à des États utilisent déjà les LLM pour la reconnaissance et les scripts d’attaque. Le délai entre la divulgation et l’exploitation active d’une vulnérabilité est passé de jours à des heures, en partie parce que les outils IA aident à transformer presque immédiatement l’information CVE publiée en moyen d’attaque.</p>
<p>Pour un écosystème CMS comme WordPress, où des milliers de vulnérabilités d’extensions ont été divulguées ces dernières années, chaque extension non corrigée devient une course contre la montre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-this-matters-for-curling-clubs">Pourquoi cela touche les clubs de curling<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#why-this-matters-for-curling-clubs" class="hash-link" aria-label="Lien direct vers Pourquoi cela touche les clubs de curling" title="Lien direct vers Pourquoi cela touche les clubs de curling" translate="no">​</a></h2>
<p>La présence en ligne de la plupart des clubs est gérée par un petit groupe de bénévoles. Ils n’ont ni équipe informatique dédiée ni budget de sécurité. Lorsqu’une extension WordPress introduit une vulnérabilité, ils peuvent ne rien en savoir jusqu’à ce que leur site soit défiguré, redirige les membres vers des pages d’hameçonnage ou recueille discrètement des renseignements de paiement.</p>
<p>Le fardeau d’entretien est important même sans incident :</p>
<ul>
<li class=""><strong>Mises à jour constantes.</strong> Le cœur de WordPress, les thèmes et les extensions doivent être mis à jour régulièrement. Les retards ouvrent la porte aux piratages.</li>
<li class=""><strong>Multiplication des extensions.</strong> Un formulaire de contact? Une extension. Des inscriptions aux événements? Une extension. Une galerie de photos? Une extension. Chacune est un vecteur d’attaque potentiel.</li>
<li class=""><strong>Gestion de l’hébergement.</strong> Vous êtes responsable de tout : système d’exploitation, serveur web, version PHP, base de données, cœur du CMS et chaque extension.</li>
<li class=""><strong>Sauvegardes et récupération.</strong> En cas de problème, il vous faut une sauvegarde récente et quelqu’un qui sait la restaurer.</li>
</ul>
<p>Un CMS généraliste est conçu pour créer toutes sortes de sites. Cette souplesse apporte une complexité dont la plupart des clubs n’ont pas besoin pour un site d’information souvent assez simple.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="if-you-insist-on-using-a-cms">Si vous tenez à utiliser un CMS<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#if-you-insist-on-using-a-cms" class="hash-link" aria-label="Lien direct vers Si vous tenez à utiliser un CMS" title="Lien direct vers Si vous tenez à utiliser un CMS" translate="no">​</a></h2>
<p>Certains clubs ont peut-être des raisons de garder WordPress ou un autre CMS. Dans ce cas, faites de la sécurité une priorité non négociable.</p>
<p>Si quelqu’un d’autre gère le site, qu’il s’agisse d’un bénévole, d’un développeur local ou d’une agence, posez-lui des questions sur chacun des points suivants. Une réponse vague ou désinvolte est un signal d’alarme. La réputation de votre club et les données de vos membres sont en jeu.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="keep-everything-updated">Gardez tout à jour<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#keep-everything-updated" class="hash-link" aria-label="Lien direct vers Gardez tout à jour" title="Lien direct vers Gardez tout à jour" translate="no">​</a></h3>
<p>Activez les mises à jour automatiques du cœur du CMS et mettez les extensions à jour dans les 24 à 48 heures suivant la publication des correctifs de sécurité. Abonnez-vous aux avis de <a href="https://www.wordfence.com/blog/" target="_blank" rel="noopener noreferrer" class="">Wordfence</a> ou de <a href="https://patchstack.com/whitepaper/state-of-wordpress-security-in-2024/" target="_blank" rel="noopener noreferrer" class="">Patchstack</a> pour WordPress, de la <a href="https://developer.joomla.org/security-centre.html" target="_blank" rel="noopener noreferrer" class="">Joomla Security Strike Team</a> ou de la <a href="https://www.drupal.org/security" target="_blank" rel="noopener noreferrer" class="">Drupal Security Team</a>. Un site non corrigé attend simplement d’être compromis.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="minimize-your-attack-surface">Réduisez votre surface d’attaque<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#minimize-your-attack-surface" class="hash-link" aria-label="Lien direct vers Réduisez votre surface d’attaque" title="Lien direct vers Réduisez votre surface d’attaque" translate="no">​</a></h3>
<p>Utilisez le moins d’extensions possible. Chaque extension installée est du code auquel vous confiez votre site et les données de vos membres. Avant toute installation, vérifiez la dernière mise à jour, le nombre d’installations et les vulnérabilités connues. Supprimez ce que vous n’utilisez pas au lieu de simplement le désactiver.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="use-a-web-application-firewall">Utilisez un pare-feu d’applications web<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#use-a-web-application-firewall" class="hash-link" aria-label="Lien direct vers Utilisez un pare-feu d’applications web" title="Lien direct vers Utilisez un pare-feu d’applications web" translate="no">​</a></h3>
<p>Placez un pare-feu d’applications web infonuagique (WAF) comme <a href="https://www.cloudflare.com/" target="_blank" rel="noopener noreferrer" class="">Cloudflare</a> devant votre site. Il peut bloquer les méthodes d’exploitation connues avant même l’installation d’une mise à jour, offrant une protection contre les attaques de type jour zéro.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="harden-authentication">Renforcez l’authentification<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#harden-authentication" class="hash-link" aria-label="Lien direct vers Renforcez l’authentification" title="Lien direct vers Renforcez l’authentification" translate="no">​</a></h3>
<p>Imposez des mots de passe forts et activez l’authentification à deux facteurs pour tous les comptes administratifs. Limitez les tentatives de connexion contre les attaques par force brute. Changez le nom d’utilisateur administratif par défaut. Ces mesures de base bloquent un nombre surprenant d’attaques.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="use-managed-hosting">Choisissez un hébergement géré<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#use-managed-hosting" class="hash-link" aria-label="Lien direct vers Choisissez un hébergement géré" title="Lien direct vers Choisissez un hébergement géré" translate="no">​</a></h3>
<p>Si possible, choisissez un hébergeur géré spécialisé dans votre CMS, comme WP Engine ou Kinsta pour WordPress et Pantheon pour Drupal. Ils gèrent la sécurité du serveur et les sauvegardes automatiques, et incluent souvent l’analyse des logiciels malveillants. Cela coûte plus cher qu’un hébergement partagé à bas prix, mais la sécurité et la tranquillité d’esprit en valent la peine pour une organisation bénévole.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="monitor-your-site">Surveillez votre site<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#monitor-your-site" class="hash-link" aria-label="Lien direct vers Surveillez votre site" title="Lien direct vers Surveillez votre site" translate="no">​</a></h3>
<p>Configurez des alertes pour les modifications de fichiers non autorisées et la création inattendue de comptes administratifs. Faites régulièrement des analyses de sécurité. Préparez un plan en cas de compromission : qui contacter et où se trouvent les sauvegardes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="consider-a-simpler-alternative">Envisagez une solution plus simple<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#consider-a-simpler-alternative" class="hash-link" aria-label="Lien direct vers Envisagez une solution plus simple" title="Lien direct vers Envisagez une solution plus simple" translate="no">​</a></h2>
<p>Si le site de votre club sert surtout à afficher les heures d’ouverture, les horaires de glace, les coordonnées et les nouvelles, vous n’avez peut-être pas besoin d’un CMS. Deux bonnes options existent :</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="a-static-site">Un site statique<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#a-static-site" class="hash-link" aria-label="Lien direct vers Un site statique" title="Lien direct vers Un site statique" translate="no">​</a></h3>
<p>Des outils comme <a href="https://gohugo.io/" target="_blank" rel="noopener noreferrer" class="">Hugo</a>, Eleventy ou même du HTML simple hébergé sur <a href="https://www.netlify.com/" target="_blank" rel="noopener noreferrer" class="">Netlify</a> ou <a href="https://pages.github.com/" target="_blank" rel="noopener noreferrer" class="">GitHub Pages</a> ont une surface d’attaque presque nulle. Aucune base de données à compromettre, aucun code serveur à exploiter. La performance est aussi excellente, sans traitement serveur ou requêtes de base de données pour ralentir le site.</p>
<p>Les agents IA comme <a href="https://claude.ai/" target="_blank" rel="noopener noreferrer" class="">Claude</a>, <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer" class="">ChatGPT</a> et <a href="https://gemini.google.com/" target="_blank" rel="noopener noreferrer" class="">Gemini</a> ont facilité cette approche pour les personnes qui ne développent pas de logiciels. Si vous payez déjà pour un de ces outils, demandez-lui de créer un site statique pour votre club. Il peut vous guider, montrer des maquettes, aider à choisir l’hébergement et accompagner le déploiement. Il peut aussi intégrer nos outils de site Curling IO et nos <a class="" href="https://curling.io/fr/docs/api/overview">API</a>.</p>
<p>Demandez aussi à l’agent de documenter ce qu’il a créé, l’hébergement et les déploiements pour que le prochain bénévole puisse prendre la relève. Demandez-lui également d’enregistrer le code source dans un dépôt comme GitHub. Cela peut sembler beaucoup, mais les agents vous guideront à chaque étape. <a href="https://claude.ai/code" target="_blank" rel="noopener noreferrer" class="">Claude Code</a> est particulièrement utile si vous acceptez de payer 20 $ par mois, et sert aussi à bien d’autres choses.</p>
<p>Si votre club a besoin d’un site sur mesure que Curling IO n’offre pas, nous recommandons fortement cette approche.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="curling-ios-built-in-website-hosting">L’hébergement de site intégré à Curling IO<a href="https://curling.io/fr/blog/why-your-curling-club-shouldnt-use-a-cms#curling-ios-built-in-website-hosting" class="hash-link" aria-label="Lien direct vers L’hébergement de site intégré à Curling IO" title="Lien direct vers L’hébergement de site intégré à Curling IO" translate="no">​</a></h3>
<p>Chaque club sur Curling IO peut activer un <a class="" href="https://curling.io/fr/docs/website/studio">site hébergé</a> sans frais supplémentaires. Il est volontairement minimaliste et convient surtout aux clubs qui n’ont pas besoin d’une multitude de fonctions accessoires.</p>
<p>Il comprend une page d’accueil personnalisable, des pages, des articles de nouvelles, des sections de commanditaires et du contenu réservé aux membres. Le tout est étroitement intégré à vos inscriptions, ligues et bonspiels. Aucun écosystème d’extensions à exploiter, aucun environnement PHP à maintenir, aucune mise à jour de CMS à manquer. Vous pouvez même y rediriger votre domaine existant.</p>
<p>Les deux approches éliminent toute la catégorie de vulnérabilités décrite ici. Le temps de vos bénévoles est mieux investi dans la gestion du club que dans les correctifs logiciels.</p>]]></content:encoded>
            <category>Gestion de club</category>
            <category>Sécurité</category>
            <category>Site web</category>
        </item>
        <item>
            <title><![CDATA[Automatiser la gestion du club avec l’IA]]></title>
            <link>https://curling.io/fr/blog/automate-club-management-with-ai</link>
            <guid>https://curling.io/fr/blog/automate-club-management-with-ai</guid>
            <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Imaginez que vous êtes gestionnaire de club et préparez la prochaine saison. Vous ouvrez votre agent IA et écrivez, ou dites simplement :]]></description>
            <content:encoded><![CDATA[<p>Imaginez que vous êtes gestionnaire de club et préparez la prochaine saison. Vous ouvrez votre agent IA et écrivez, ou dites simplement :</p>
<blockquote>
<p>Prépare un rabais d’inscription hâtive pour la Ligue du mardi soir. Un rabais de 15 % pour les inscriptions avant le 1er septembre.</p>
</blockquote>
<p>Cinq secondes plus tard, c’est fait. Aucun onglet de navigateur, aucun formulaire, aucun menu à parcourir. Avec Curling IO v3, vous pourrez le faire.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-agent-in-action">L’agent en action<a href="https://curling.io/fr/blog/automate-club-management-with-ai#the-agent-in-action" class="hash-link" aria-label="Lien direct vers L’agent en action" title="Lien direct vers L’agent en action" translate="no">​</a></h2>
<p>Cette conversation se déroule après votre connexion à Curling IO et l’autorisation de l’interface en ligne de commande <code>curling</code>, expliquée plus bas :</p>
<blockquote>
<p><strong>Vous :</strong> Aide-moi à préparer la nouvelle saison. Prépare un rabais d’inscription hâtive pour la Ligue du mardi soir : 15 % de rabais pour les inscriptions avant le 1er septembre.</p>
</blockquote>
<div class="theme-admonition theme-admonition-info admonition_HXej alert alert--info"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Agent</div><div class="admonitionContent_Jlkn"><details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Deux commandes exécutées</summary><div><div class="collapsibleContent_ZBPi"><div class="language-sh codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-sh codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.items.list --org vancouver</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling commands admin.discounts.create</span><br></div></code></pre></div></div></div></div></details><p>J’ai trouvé la Ligue du mardi soir (2026-27). Je crée le rabais d’inscription hâtive.</p><details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Une commande exécutée</summary><div><div class="collapsibleContent_ZBPi"><div class="language-sh codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-sh codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.discounts.create --org vancouver '{"params": {...}}'</span><br></div></code></pre></div></div></div></div></details><p>C’est fait. Un rabais de 15 % pour la Ligue du mardi soir, valide jusqu’au 1er septembre. Je vois aussi une ligue Mixte du lundi et une ligue Doubles du vendredi cette saison. Voulez-vous aussi un rabais d’inscription hâtive pour celles-ci?</p></div></div>
<blockquote>
<p><strong>Vous :</strong> La même chose pour le Mixte du lundi. Laisse tomber le vendredi, cette ligue se remplit toute seule.</p>
</blockquote>
<div class="theme-admonition theme-admonition-info admonition_HXej alert alert--info"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Agent</div><div class="admonitionContent_Jlkn"><details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Une commande exécutée</summary><div><div class="collapsibleContent_ZBPi"><div class="language-sh codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-sh codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.discounts.create --org vancouver '{"params": {...}}'</span><br></div></code></pre></div></div></div></div></details><p>C’est fait. Le Mixte du lundi a le même rabais de 15 %. À propos, la Ligue du mardi soir de la saison dernière a encore trois personnes sur sa liste d’attente. Voulez-vous que je leur envoie un courriel pour annoncer l’ouverture des inscriptions de la nouvelle saison?</p></div></div>
<blockquote>
<p><strong>Vous :</strong> Oui, fais-le.</p>
</blockquote>
<div class="theme-admonition theme-admonition-info admonition_HXej alert alert--info"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Agent</div><div class="admonitionContent_Jlkn"><details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Deux commandes exécutées</summary><div><div class="collapsibleContent_ZBPi"><div class="language-sh codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-sh codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.waitlists.list --org vancouver '{"item_id": 31}'</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.emails.send --org vancouver '{"params": {...}}'</span><br></div></code></pre></div></div></div></div></details><p>C’est envoyé. Trois courriels sont partis aux curleurs sur la liste d’attente de la Ligue du mardi soir de la saison dernière. Ils annoncent l’ouverture de la saison 2026-27 avec un rabais d’inscription hâtive jusqu’au 1er septembre.</p></div></div>
<blockquote>
<p><strong>Vous :</strong> Où en sont les inscriptions cette saison?</p>
</blockquote>
<div class="theme-admonition theme-admonition-info admonition_HXej alert alert--info"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Agent</div><div class="admonitionContent_Jlkn"><details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Une commande exécutée</summary><div><div class="collapsibleContent_ZBPi"><div class="language-sh codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-sh codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">$ curling call admin.items.list --org vancouver '{"season": "2026-27"}'</span><br></div></code></pre></div></div></div></div></details><p>Voici la situation pour 2026-27 :</p><table><thead><tr><th>Ligue</th><th>Inscriptions</th><th>Capacité</th><th>Statut</th></tr></thead><tbody><tr><td>Mixte du lundi</td><td>14</td><td>72</td><td>Ouvert</td></tr><tr><td>Ligue du mardi soir</td><td>23</td><td>96</td><td>Ouvert</td></tr><tr><td>Doubles du vendredi</td><td>52</td><td>64</td><td>Ouvert</td></tr></tbody></table><p>La ligue Doubles du vendredi est déjà à 81 % de sa capacité, sans rabais. Les inscriptions aux ligues du mardi et du lundi sont encore à leurs débuts. Voulez-vous que je surveille les inscriptions et vous prévienne lorsqu’une ligue dépasse 80 %?</p></div></div>
<blockquote>
<p><strong>Vous :</strong> Oui, fais le point avec moi chaque semaine.</p>
</blockquote>
<p>Préparation de saison, communications ciblées et rapports dans une conversation de cinq minutes.</p>
<p>La même approche permet de préparer les événements d’une saison complète, modifier les frais de plusieurs ligues à la fois, produire des rapports financiers ou traiter les listes d’attente. Tout ce qu’un administrateur peut faire dans l’interface web, un agent peut le faire en ligne de commande.</p>
<p>Ce n’est pas théorique. Nous le développons en ce moment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="how-it-works">Le fonctionnement<a href="https://curling.io/fr/blog/automate-club-management-with-ai#how-it-works" class="hash-link" aria-label="Lien direct vers Le fonctionnement" title="Lien direct vers Le fonctionnement" translate="no">​</a></h2>
<p>En coulisses, l’agent appelle un outil en ligne de commande nommé <code>curling</code>. Tout agent IA ayant accès à un interpréteur de commandes peut l’appeler : OpenClaw, NemoClaw, Claude Code ou Codex.</p>
<p>Nous avons envisagé MCP (Model Context Protocol), une norme de connexion des modèles IA à des outils externes. Nous avons créé un prototype fonctionnel. Mais une interface en ligne de commande est plus simple, plus portable et fonctionne avec tous les cadriciels d’agents, au-delà de ceux compatibles avec MCP. MCP pourra arriver plus tard comme mince couche supplémentaire.</p>
<p>L’outil se connecte au même serveur que l’administration web Curling IO. Chaque opération d’administration disponible dans le navigateur l’est aussi en ligne de commande. Lorsque nous ajoutons une fonction à l’administration, elle devient automatiquement accessible aux agents sans mise à jour du client.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="authentication">Authentification<a href="https://curling.io/fr/blog/automate-club-management-with-ai#authentication" class="hash-link" aria-label="Lien direct vers Authentification" title="Lien direct vers Authentification" translate="no">​</a></h2>
<p>Avant qu’un agent gère votre club, vous lui donnez accès. Une seule fois, en environ 30 secondes.</p>
<div class="language-text codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-text codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curling login</span><br></div></code></pre></div></div>
<p>L’outil affiche une URL. Vous l’ouvrez, vous vous connectez à Curling IO et approuvez l’accès. C’est tout. L’outil stocke un jeton et gère ensuite son renouvellement silencieusement.</p>
<p>Il utilise le parcours d’autorisation d’appareil OAuth 2.1 (RFC 8628), la même norme que GitHub CLI, Google Cloud CLI et AWS SSO. Les équipes de sécurité la connaissent bien, et elle fonctionne dans tous les environnements.</p>
<p>Après la connexion, l’agent découvre les clubs que vous gérez :</p>
<div class="language-text codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-text codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curling orgs</span><br></div></code></pre></div></div>
<p>Il limite ensuite la portée de ses commandes en conséquence :</p>
<div class="language-text codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-text codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curling call admin.events.list --org vancouver</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="who-its-for">À qui cela s’adresse<a href="https://curling.io/fr/blog/automate-club-management-with-ai#who-its-for" class="hash-link" aria-label="Lien direct vers À qui cela s’adresse" title="Lien direct vers À qui cela s’adresse" translate="no">​</a></h2>
<p>La véritable cible est un agent IA qui agit pour un gestionnaire de club. Les utilisateurs naturels de l’outil sont les administrateurs qui adoptent tôt les outils IA et utilisent déjà des agents pour gérer leurs courriels, rédiger des communications et automatiser les tâches répétitives. Leur donner accès à <code>curling</code> est la prochaine étape logique.</p>
<p>Nous concevons l’outil comme si un expert humain pouvait l’utiliser directement. Aucun assistant de configuration, aucun accompagnement pas à pas. Une sortie concise et précise. Un <code>--help</code> complet qui récompense la lecture. Les agents évoluent vers des comportements humains, alors une bonne interface en ligne de commande sert les deux publics.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-bigger-picture">La vue d’ensemble<a href="https://curling.io/fr/blog/automate-club-management-with-ai#the-bigger-picture" class="hash-link" aria-label="Lien direct vers La vue d’ensemble" title="Lien direct vers La vue d’ensemble" translate="no">​</a></h2>
<p>Les logiciels de gestion de club ont toujours été conçus pour des humains qui naviguent dans des interfaces web. C’est la bonne conception pour la plupart des utilisateurs la plupart du temps. Mais les administrateurs ont du travail répétitif et structuré : préparer une saison, modifier les tarifs en lot, produire des rapports et approuver des files d’inscription. Un agent peut s’en charger.</p>
<p>La plupart des clubs n’y pensent pas encore, mais nous croyons qu’ils le feront dans les prochaines années, à mesure que les outils et agents IA prendront de la place. À court terme, seuls quelques gestionnaires à l’aise avec la technologie essaieront probablement, et ils ne reviendront probablement jamais en arrière.</p>]]></content:encoded>
            <category>IA</category>
            <category>v3</category>
            <category>Interface</category>
            <category>Gestion de club</category>
        </item>
        <item>
            <title><![CDATA[Les formulaires d’inscription personnalisés arrivent dans Curling IO]]></title>
            <link>https://curling.io/fr/blog/drag-and-drop-registration-forms</link>
            <guid>https://curling.io/fr/blog/drag-and-drop-registration-forms</guid>
            <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un premier aperçu du générateur de formulaires par glisser-déposer de Curling IO v3. Les administrateurs conçoivent les formulaires d’inscription en plaçant les questions sur un canevas visuel.]]></description>
            <content:encoded><![CDATA[<p><em>Cet article fait partie de notre <a class="" href="https://curling.io/fr/blog/tags/sneak-peek">série d’aperçus</a> de Curling IO v3, où nous explorons certaines fonctions offertes dans la prochaine version.</em></p>
<p>Les formulaires d’inscription de Curling IO ont toujours recueilli les renseignements de base : nom d’équipe, alignement, niveau de jeu et coordonnées. Mais chaque club fonctionne un peu différemment. Certains ont besoin de contacts d’urgence. D’autres veulent connaître les restrictions alimentaires pour planifier un banquet. Un bonspiel peut demander les coordonnées de l’équipe alors qu’une ligue n’en a pas besoin.</p>
<p>Dans la version 2, les administrateurs peuvent déjà choisir les questions affichées et en créer. La nouveauté de la version 3 est le contrôle de la mise en page : la position et la largeur de chaque question, ainsi que l’organisation du formulaire en sections.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="how-it-works">Le fonctionnement<a href="https://curling.io/fr/blog/drag-and-drop-registration-forms#how-it-works" class="hash-link" aria-label="Lien direct vers Le fonctionnement" title="Lien direct vers Le fonctionnement" translate="no">​</a></h2>
<p>Le générateur de formulaires est un outil de glisser-déposer dans l’administration du produit. À gauche, un panneau de questions disponibles. À droite, un canevas vide. Faites glisser une question sur le canevas pour l’ajouter au formulaire d’inscription. Ramenez-la dans le panneau pour la retirer. Réorganisez les questions en les déplaçant dans le canevas.</p>
<p><img decoding="async" loading="lazy" alt="L’interface d’administration du générateur de formulaires" src="https://curling.io/fr/assets/images/form-builder-admin-f73eab20ba1adf72a20fd039b7c2cfab.png" width="2921" height="1645" class="img_amcT"></p>
<p>Chaque question a une largeur définie de 1, 2 ou 3 colonnes qui détermine sa place dans la grille. Une question d’une colonne, comme « Pointure », occupe le tiers de la rangée. Une question de deux colonnes, comme « Adresse municipale », en occupe les deux tiers. Une question de trois colonnes, comme « Notes », prend toute la largeur. La grille de type maçonnerie rapproche les questions sans laisser de trous.</p>
<p>Les séparateurs divisent visuellement le formulaire en sections. Les questions au-dessus d’un séparateur y restent. Une nouvelle section commence sous celui-ci.</p>
<p>Chaque question peut être obligatoire ou facultative. Les questions obligatoires portent un astérisque dans le formulaire public et doivent être remplies avant l’envoi.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-curlers-see">Ce que voient les curleurs<a href="https://curling.io/fr/blog/drag-and-drop-registration-forms#what-curlers-see" class="hash-link" aria-label="Lien direct vers Ce que voient les curleurs" title="Lien direct vers Ce que voient les curleurs" translate="no">​</a></h2>
<p>Le formulaire public affiche la mise en page conçue par l’administrateur. Quand vous commencez à saisir du texte, le libellé de la question remonte au-dessus du champ pour garder le formulaire compact. Les indications prennent la forme d’icônes d’information qui se développent au survol.</p>
<p><img decoding="async" loading="lazy" alt="Le formulaire d’inscription public" src="https://curling.io/fr/assets/images/form-builder-public-601b03d3dd5f0920af4c1443ad9b4705.png" width="2582" height="1538" class="img_amcT"></p>
<p>Le formulaire s’adapte à l’écran. Sur téléphone, tout s’empile en une colonne. Sur tablette, deux colonnes. Sur ordinateur, les trois colonnes s’affichent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="available-questions">Les questions disponibles<a href="https://curling.io/fr/blog/drag-and-drop-registration-forms#available-questions" class="hash-link" aria-label="Lien direct vers Les questions disponibles" title="Lien direct vers Les questions disponibles" translate="no">​</a></h2>
<p>Le générateur comprend un catalogue de questions prédéfinies : coordonnées, contacts d’urgence, renseignements médicaux, questions propres au sport, tailles de vêtements, restrictions alimentaires et plus encore. Les clubs peuvent aussi créer leurs propres questions pour recueillir les autres renseignements nécessaires. La liste permet la recherche, et tout produit peut utiliser une combinaison de questions prédéfinies et personnalisées.</p>
<p>Certaines questions sont liées aux paramètres de l’événement. Si l’administrateur configure un libellé de nom d’équipe, la question correspondante apparaît automatiquement sur le canevas et reste verrouillée. C’est la même chose pour la question d’alignement lorsqu’une option d’alignement est sélectionnée. Désactiver ces paramètres retire les questions verrouillées du générateur.</p>
<p>Les éléments <strong>Espace</strong> et <strong>Séparateur</strong> servent à la mise en page; ils ne recueillent aucun renseignement. Ajoutez un espace pour laisser une cellule vide dans la grille. Ajoutez un séparateur pour diviser visuellement le formulaire en sections.</p>
<p>Le générateur de formulaires fait partie de Curling IO v3. Nous présenterons d’autres fonctions de la version 3 à l’approche du lancement.</p>]]></content:encoded>
            <category>v3</category>
            <category>Inscriptions</category>
            <category>Créateur de formulaires</category>
            <category>Aperçus</category>
        </item>
        <item>
            <title><![CDATA[Une administration en direct avec Gleam et Lustre]]></title>
            <link>https://curling.io/fr/blog/live-admin-without-javascript</link>
            <guid>https://curling.io/fr/blog/live-admin-without-javascript</guid>
            <pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. Pourquoi nous avons choisi Rust pour la version 3 explique cette décision et ce que nous avons conservé de cette exploration.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Mise à jour : 1er octobre 2026</div><div class="admonitionContent_Jlkn"><p>Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. <a class="" href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3">Pourquoi nous avons choisi Rust pour la version 3</a> explique cette décision et ce que nous avons conservé de cette exploration.</p></div></div>
<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cet article d’architecture technique vient du développement de Curling IO v3. Il s’adresse aux ingénieurs logiciels et aborde Gleam, Lustre, BEAM, les WebSockets et les interfaces rendues côté serveur plus en détail que nos articles habituels sur le produit.</p></div></div>
<p>Le panneau d’administration de Curling IO devrait réagir instantanément lorsqu’un gestionnaire prépare une saison. Activer un paramètre, enregistrer un rabais, passer d’une section de produit à l’autre : la page devrait répondre sans rechargement complet.</p>
<p>La version 2 fonctionne, mais chaque envoi de formulaire recharge la page. L’administration de la version 3 est un seul composant serveur Lustre sur BEAM : une connexion WebSocket et un processus Erlang de longue durée par session. Chaque interaction passe par ce WebSocket et revient sous forme de modification du DOM. La page ne se recharge jamais, sans cadriciel JavaScript côté client.</p>
<!-- -->
<figure><p><img decoding="async" loading="lazy" alt="Le panneau d’administration Curling IO avec la page d’inscription d’un produit" src="https://curling.io/fr/assets/images/product-registration-7269779dd777678c74ef9f6dae689ce5.png" width="1888" height="1308" class="img_amcT"></p><figcaption><em>La page d’inscription d’un produit : barre latérale, fil d’Ariane, interrupteurs et champs de formulaire, tous rendus côté serveur par une seule connexion WebSocket. Chaque interrupteur, saisie et enregistrement met l’état à jour en direct.</em></figcaption></figure>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="whats-a-server-component">Qu’est-ce qu’un composant serveur?<a href="https://curling.io/fr/blog/live-admin-without-javascript#whats-a-server-component" class="hash-link" aria-label="Lien direct vers Qu’est-ce qu’un composant serveur?" title="Lien direct vers Qu’est-ce qu’un composant serveur?" translate="no">​</a></h2>
<p>Phoenix LiveView a popularisé l’idée : produire le HTML sur le serveur, envoyer les modifications au client par WebSocket et traiter les événements de la même façon. Le navigateur devient une mince couche d’affichage. Lustre, le cadriciel d’interface de Gleam, intègre cette notion sous le nom de « composants serveur ».</p>
<p>Un composant serveur Lustre est une application suivant l’architecture Elm (modèle, mise à jour, vue) qui fonctionne comme processus BEAM. Lorsque le modèle change, Lustre compare l’ancien et le nouveau DOM virtuel, puis envoie une modification JSON par WebSocket. Le client l’applique. Les événements du navigateur, clics, envois de formulaire et changements de saisie, retournent en JSON. Toute la boucle prend moins de dix millisecondes sur un réseau local.</p>
<p>Côté client, un élément personnalisé nommé <code>&lt;lustre-server-component&gt;</code> se charge de tout. Vous lui indiquez une route WebSocket, et il crée le DOM fantôme (shadow DOM), adopte les feuilles de style, applique les modifications et transmet les événements. Lustre fournit l’environnement d’exécution client dans un seul fichier JavaScript à inclure dans la page.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-architecture">L’architecture<a href="https://curling.io/fr/blog/live-admin-without-javascript#the-architecture" class="hash-link" aria-label="Lien direct vers L’architecture" title="Lien direct vers L’architecture" translate="no">​</a></h2>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Raccordement du composant serveur</summary><div><div class="collapsibleContent_ZBPi"><p>L’administration charge une structure HTML minimale :</p><div class="language-html codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-html codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">html</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">head</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">link</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">rel</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(199, 146, 234)">=</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag attr-value" style="color:rgb(255, 85, 114)">stylesheet</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">href</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(199, 146, 234)">=</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag attr-value" style="color:rgb(255, 85, 114)">/static/css/app.css</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">head</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">body</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">lustre-server-component</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">route</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(199, 146, 234)">=</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag attr-value" style="color:rgb(255, 85, 114)">/ws/admin?url=/en/admin/products</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">lustre-server-component</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">script</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">src</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(199, 146, 234)">=</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag attr-value" style="color:rgb(255, 85, 114)">/static/lustre-server-component.mjs</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token script"></span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">script</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;</span><span class="token tag" style="color:rgb(255, 85, 114)">script</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">src</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(199, 146, 234)">=</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag attr-value" style="color:rgb(255, 85, 114)">/static/js/admin-live.js</span><span class="token tag attr-value punctuation" style="color:rgb(199, 146, 234)">"</span><span class="token tag" style="color:rgb(255, 85, 114)"> </span><span class="token tag attr-name" style="color:rgb(255, 203, 107)">defer</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token script"></span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">script</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">body</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&lt;/</span><span class="token tag" style="color:rgb(255, 85, 114)">html</span><span class="token tag punctuation" style="color:rgb(199, 146, 234)">&gt;</span><br></div></code></pre></div></div><p>C’est tout le HTML envoyé par le serveur. Le reste est rendu par WebSocket.</p><p>Lorsque le navigateur ouvre la page, l’élément personnalisé Lustre se connecte à <code>/ws/admin</code>, puis la connexion passe au protocole WebSocket. Côté serveur, Mist, le serveur HTTP, transmet la connexion à notre gestionnaire WebSocket, qui démarre un environnement d’exécution Lustre :</p><div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">fn(_connection) {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(runtime) =</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    lustre.start_server_component(admin.app(), flags)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let self = process.new_subject()</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let selector =</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    process.new_selector()</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    |&gt; process.select(for: self)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  server_component.register_subject(self)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  |&gt; lustre.send(to: runtime)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  #(SocketState(runtime:, self:), Some(selector))</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div><p><code>lustre.start_server_component</code> crée un acteur qui conserve l’état de l’application, exécute la fonction de mise à jour à chaque message, compare les vues et émet les modifications. L’appel <code>register_subject</code> indique à l’environnement d’exécution d’envoyer ses modifications à cette connexion WebSocket. Tout devient alors bidirectionnel :</p><ul>
<li class=""><strong>Du navigateur au serveur :</strong> l’utilisateur clique sur un lien. Le client envoie un événement JSON. Mist le décode et le transmet à l’acteur Lustre.</li>
<li class=""><strong>Du serveur au navigateur :</strong> l’acteur met le modèle à jour, compare la vue et retourne une modification JSON. Le client l’applique au DOM fantôme.</li>
</ul><p>Chaque session d’administration a son propre processus BEAM. Les sessions ne partagent rien : aucun stockage de session, aucun mécanisme de publication et d’abonnement, aucun état à coordonner. Si une session plante, les autres ne le remarquent pas. C’est l’isolation des processus BEAM en action.</p></div></div></details>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="navigation-without-page-loads">Naviguer sans charger les pages<a href="https://curling.io/fr/blog/live-admin-without-javascript#navigation-without-page-loads" class="hash-link" aria-label="Lien direct vers Naviguer sans charger les pages" title="Lien direct vers Naviguer sans charger les pages" translate="no">​</a></h2>
<p>Les pages rendues côté serveur se rechargent habituellement à chaque navigation. Cela nuit à un panneau d’administration où l’on passe constamment d’une section de produit à l’autre. Il nous fallait le comportement de navigation côté client, avec pushState et le bouton de retour, sans routeur client.</p>
<p>La solution tient en environ 90 lignes de JavaScript sans cadriciel dans <code>admin-live.js</code>. Elles relient l’API d’historique du navigateur au composant serveur et gèrent trois parcours :</p>
<p><strong>Clics sur les liens.</strong> Un écouteur de clics sur le composant serveur intercepte les balises <code>&lt;a&gt;</code>, empêche le chargement, appelle <code>history.pushState</code> et informe le serveur de la nouvelle URL. Les touches de modification, comme cmd+clic, les liens externes et <code>target="_blank"</code> sont laissés au navigateur. L’écouteur utilise <code>composedPath()</code> pour franchir la frontière du DOM fantôme et trouver le véritable élément <code>&lt;a&gt;</code>.</p>
<p><strong>Boutons précédent et suivant.</strong> Un écouteur <code>popstate</code> détecte la navigation par les boutons du navigateur et envoie la nouvelle URL au serveur sans l’ajouter à l’historique, déjà mis à jour par le navigateur.</p>
<p><strong>Navigation déclenchée par le serveur.</strong> Lorsqu’un enregistrement redirige vers une autre page, par exemple du formulaire de modification à l’aperçu, le serveur émet un événement personnalisé avec la nouvelle URL. Le client l’intercepte et appelle <code>pushState</code>.</p>
<p>Les trois parcours communiquent avec le composant serveur par la même astuce : des éléments <code>&lt;input&gt;</code> cachés dans le DOM fantôme. JavaScript définit la valeur du champ à la nouvelle URL et déclenche un événement <code>change</code>. Le gestionnaire d’événements Lustre côté serveur le reçoit. Les événements DOM ordinaires ne franchissent pas cette frontière de façon fiable, mais ceux des éléments de formulaire remontent dans les mécanismes internes du composant. Nous avions d’abord essayé des événements personnalisés et passé du temps à déboguer avant d’arriver à cette solution.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="shadow-dom-and-stylesheets">DOM fantôme et feuilles de style<a href="https://curling.io/fr/blog/live-admin-without-javascript#shadow-dom-and-stylesheets" class="hash-link" aria-label="Lien direct vers DOM fantôme et feuilles de style" title="Lien direct vers DOM fantôme et feuilles de style" translate="no">​</a></h2>
<p>Lustre produit le rendu dans un DOM fantôme, ce qui offre une véritable encapsulation. Les styles ne traversent pas la frontière dans un sens ou l’autre. Le CSS de l’administration n’affecte pas le reste de la page, et vice versa. Mais il faut faire entrer les feuilles de style dans la racine du DOM fantôme.</p>
<p>Lustre s’en charge automatiquement avec <code>adoptedStyleSheets</code>, une API du navigateur qui permet aux racines de DOM fantôme de partager les feuilles de style du document parent. Au montage, le composant parcourt toutes les feuilles du document et les ajoute au tableau <code>adoptedStyleSheets</code> de sa racine. Si une feuille ne peut pas être adoptée directement à cause des restrictions d’origine, il copie les règles CSS dans une nouvelle feuille. Si cela échoue aussi, il clone l’élément <code>&lt;link&gt;</code> ou <code>&lt;style&gt;</code> dans la racine. Trois stratégies, par ordre de préférence.</p>
<p>Cela a fonctionné directement avec Tailwind et notre bibliothèque de composants Basecoat. Un point nous a fait trébucher : les propriétés CSS personnalisées. Nos variantes de couleur d’alerte utilisaient <code>--success</code>, <code>--warning</code> et <code>--info</code>, mais ces variables n’étaient jamais définies. Les styles étaient bien adoptés, mais <code>color: var(--success)</code> ne donnait rien, car la variable n’existait pas. Il a suffi d’ajouter les définitions à <code>:root</code>. Les propriétés CSS personnalisées sont héritées à travers les frontières du DOM fantôme, contrairement aux règles CSS ordinaires. Une fois définies dans <code>:root</code>, elles sont donc disponibles partout.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-runs-on-the-server-what-runs-on-the-client">Ce qui fonctionne sur le serveur et sur le client<a href="https://curling.io/fr/blog/live-admin-without-javascript#what-runs-on-the-server-what-runs-on-the-client" class="hash-link" aria-label="Lien direct vers Ce qui fonctionne sur le serveur et sur le client" title="Lien direct vers Ce qui fonctionne sur le serveur et sur le client" translate="no">​</a></h2>
<p>La séparation est simple. Le serveur gère tout ce qui ne demande pas de rétroaction visuelle en moins d’une image d’affichage :</p>
<ul>
<li class="">Navigation et routage</li>
<li class="">Saisie et validation des formulaires</li>
<li class="">Interrupteurs de fonctions</li>
<li class="">Chargement des données, synchrone depuis SQLite</li>
<li class="">Alertes temporaires</li>
<li class="">Ouverture et fermeture de la barre latérale</li>
</ul>
<p>Le client gère le raccordement de la navigation, soit 90 lignes de JavaScript, et l’adoption des feuilles de style intégrée à Lustre. C’est tout. Toute l’interface d’administration est écrite en Gleam, y compris la barre latérale, le fil d’Ariane, les champs, les tableaux et les alertes.</p>
<figure><p><img decoding="async" loading="lazy" alt="Le créateur de tableau de Curling IO" src="https://curling.io/fr/assets/images/bracket-builder-751ac4db217b75cb64be032f27d141b7.png" width="1570" height="1156" class="img_amcT"></p><figcaption><em>Le <strong>Créateur de tableau</strong> de Curling IO. Ce type d’interaction par glisser-déposer exige un rendu côté client.</em></figcaption></figure>
<p>Nous excluons explicitement du composant serveur les minuteries, le glisser-déposer, les animations et les observateurs de redimensionnement. Leurs boucles de rétroaction côté client surchargeraient le WebSocket. Le créateur de tableaux et le glisser-déposer d’équipes seront des « îlots » Lustre : de petites applications Gleam côté client, compilées en JavaScript et intégrées à certaines pages. Le composant serveur rend la page; l’îlot gère le widget interactif.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-we-got-wrong-along-the-way">Nos erreurs en cours de route<a href="https://curling.io/fr/blog/live-admin-without-javascript#what-we-got-wrong-along-the-way" class="hash-link" aria-label="Lien direct vers Nos erreurs en cours de route" title="Lien direct vers Nos erreurs en cours de route" translate="no">​</a></h2>
<p><strong>Boucle infinie du bouton de retour.</strong> Notre première navigation utilisait un seul message <code>NavigateTo</code> pour tout. Le serveur demandait au client d’ajouter une URL à l’historique, et le client le faisait. L’utilisateur revenait en arrière, le navigateur déclenchait popstate, le client envoyait l’ancienne URL au serveur, qui émettait un autre pushState. Une boucle. La solution : séparer <code>NavigateTo</code>, qui ajoute à l’historique, d’<code>UrlChanged</code>, qui ne le fait pas.</p>
<p><strong>Clignotement de la barre latérale.</strong> Pendant le chargement entre deux pages de produit, nous effacions le contexte du produit. La barre se repliait à son état par défaut, puis se dépliait au chargement. Nous avons conservé le contexte du produit dans le modèle d’administration pendant les navigations au sein du même produit.</p>
<p><strong>Disparition des messages temporaires.</strong> Enregistrer un rabais ou des frais d’association mène à la liste avec un message temporaire. Mais nous avions aussi ajouté l’effacement de ce message à la navigation. L’enregistrement définissait le message, puis appelait <code>NavigateTo</code>, qui l’effaçait. Il fallait définir le message <em>après</em> la navigation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-we-got-for-free">Ce que nous avons obtenu sans effort<a href="https://curling.io/fr/blog/live-admin-without-javascript#what-we-got-for-free" class="hash-link" aria-label="Lien direct vers Ce que nous avons obtenu sans effort" title="Lien direct vers Ce que nous avons obtenu sans effort" translate="no">​</a></h2>
<p>Le chargement de code à chaud de BEAM s’applique aux composants serveur. Lorsque l’outil de surveillance du développement recompile un module, le processus en cours utilise le nouveau code au message suivant. Le WebSocket reste connecté, l’état de la session est conservé et le prochain clic ou changement de page affiche la nouvelle vue. Nous n’avons rien créé ou configuré pour cela. Un jour, nous l’avons remarqué pendant le développement et compris que BEAM le faisait depuis le début.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-not-ssr-or-a-spa">Pourquoi pas du SSR ou une SPA?<a href="https://curling.io/fr/blog/live-admin-without-javascript#why-not-ssr-or-a-spa" class="hash-link" aria-label="Lien direct vers Pourquoi pas du SSR ou une SPA?" title="Lien direct vers Pourquoi pas du SSR ou une SPA?" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="over-ssr">Par rapport au SSR<a href="https://curling.io/fr/blog/live-admin-without-javascript#over-ssr" class="hash-link" aria-label="Lien direct vers Par rapport au SSR" title="Lien direct vers Par rapport au SSR" translate="no">​</a></h3>
<p>L’administration de la version 2 utilise le rendu serveur avec des rechargements complets. Cela fonctionne, mais chaque interaction a une latence visible. On active un interrupteur et attend le rechargement pour voir le résultat. On change de section de produit et perd sa position de défilement.</p>
<p>Avec le composant serveur, activer un interrupteur met immédiatement l’interface à jour. Le processus BEAM est là, sans aller-retour réseau vers une base de données. L’enregistrement d’un formulaire peut mener instantanément à une autre page avec un message temporaire. La barre latérale reste en place, car elle appartient à la même application en cours d’exécution.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="over-a-spa">Par rapport à une SPA<a href="https://curling.io/fr/blog/live-admin-without-javascript#over-a-spa" class="hash-link" aria-label="Lien direct vers Par rapport à une SPA" title="Lien direct vers Par rapport à une SPA" translate="no">​</a></h3>
<p>Pour l’utilisateur, le composant serveur ressemble exactement à une application monopage (SPA). La navigation ne recharge pas la page, l’état persiste et les mises à jour sont instantanées. La différence est l’endroit où le travail se fait.</p>
<p>Une SPA télécharge un paquet JavaScript avant que l’utilisateur voie quoi que ce soit. Une administration React modeste avec bibliothèque de composants, routeur et gestion d’état atteint facilement 200 à 500 Ko de JavaScript. La nôtre envoie environ 15 Ko de HTML initial par WebSocket et aucun JavaScript applicatif. L’environnement client Lustre est un script générique de 10 Ko, pas du code d’application.</p>
<p>Une SPA exige aussi une API REST ou GraphQL pour parler au serveur : points d’accès à concevoir, données à sérialiser en JSON puis à désérialiser côté client, états de chargement, cache et synchronisation des types client et serveur. Avec le composant serveur, la fonction de mise à jour a directement accès au domaine et à la logique métier. Aucune mécanique d’API ne s’interpose. Lorsqu’un formulaire est enregistré, elle écrit dans la base et retourne le nouveau modèle. La vue est comparée automatiquement. Toute l’administration est un seul ensemble de code Gleam avec l’état au même endroit.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="developer-experience">Expérience de développement<a href="https://curling.io/fr/blog/live-admin-without-javascript#developer-experience" class="hash-link" aria-label="Lien direct vers Expérience de développement" title="Lien direct vers Expérience de développement" translate="no">​</a></h3>
<p>Chaque page d’administration est un module Gleam avec <code>Model</code>, <code>Msg</code>, <code>init</code>, <code>update</code> et <code>view</code>. Les types imposent le traitement de tous les messages. Ajouter une page revient à ajouter une variante de route, dont le compilateur indique tous les endroits à traiter, un module de page et son raccordement à la fonction de mise à jour de l’administration. Aucun langage de modèles ni objets de contexte transmis par intergiciel. Des fonctions à tous les niveaux.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-numbers">Les chiffres<a href="https://curling.io/fr/blog/live-admin-without-javascript#the-numbers" class="hash-link" aria-label="Lien direct vers Les chiffres" title="Lien direct vers Les chiffres" translate="no">​</a></h2>
<p>Pour situer ce que nous utilisons :</p>
<ul>
<li class="">La structure HTML d’administration a 6 lignes de contenu : métadonnées, une feuille de style, l’élément personnalisé et deux scripts.</li>
<li class=""><code>admin-live.js</code> contient 89 lignes de JavaScript sans cadriciel.</li>
<li class="">Le montage initial envoie tout le DOM virtuel, soit environ 15 à 30 Ko selon la page.</li>
<li class="">Les modifications suivantes font généralement moins de 1 Ko, seulement les différences.</li>
<li class="">Chaque session est un seul processus BEAM qui consomme quelques centaines de Ko de mémoire.</li>
<li class="">Les requêtes SQLite fonctionnent dans le processus. Le chargement des données lors des changements de page est donc synchrone et mesuré en microsecondes.</li>
</ul>
<p>Il y a aussi un avantage de volume transmis par rapport au SSR traditionnel. Chaque requête HTTP transporte des en-têtes : témoins, type de contenu, jetons CSRF, en-têtes d’acceptation et directives de cache. Cela représente généralement 1 à 2 Ko supplémentaires par aller-retour, dans les deux sens. Une trame WebSocket ne contient que la charge utile avec quelques octets d’encadrement. Une session d’administration peut compter des centaines d’interactions : activer des interrupteurs, changer de section, enregistrer des formulaires. En HTTP, chacune paie le coût des en-têtes. En WebSocket, la connexion est déjà établie et authentifiée. Un interrupteur qui change un booléen envoie peut-être 80 octets de JSON et reçoit une modification de 200 octets. Le POST HTTP équivalent ferait 2 à 3 Ko avec les en-têtes, la redirection et le nouveau rendu complet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="whats-next">La suite<a href="https://curling.io/fr/blog/live-admin-without-javascript#whats-next" class="hash-link" aria-label="Lien direct vers La suite" title="Lien direct vers La suite" translate="no">​</a></h2>
<p>Le composant serveur couvre les 95 % des pages d’administration faites de formulaires, tableaux et paramètres. Les 5 % restants, le créateur de tableaux, la gestion des équipes par glisser-déposer et les tableaux de pointage en direct, seront des îlots Lustre côté client. Ils communiqueront avec le serveur par le même WebSocket ou par des appels d’API distincts.</p>
<hr>
<p><em>Cet article est le huitième de la série sur les fondations de Curling IO. À suivre : <a class="" href="https://curling.io/fr/blog/improving-recovery-with-nixos">améliorer la récupération avec NixOS</a>.</em></p>]]></content:encoded>
            <category>Fondations</category>
            <category>Gleam</category>
            <category>BEAM</category>
            <category>Lustre</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Les agents IA aiment Gleam]]></title>
            <link>https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam</link>
            <guid>https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam</guid>
            <pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. Pourquoi nous avons choisi Rust pour la version 3 explique cette décision et ce que nous avons conservé de cette exploration.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Mise à jour : 1er octobre 2026</div><div class="admonitionContent_Jlkn"><p>Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. <a class="" href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3">Pourquoi nous avons choisi Rust pour la version 3</a> explique cette décision et ce que nous avons conservé de cette exploration.</p></div></div>
<p><em>Vous êtes prévenus : cet article contient des opinions qui seront controversées et qui pourraient mal vieillir. Terrain dangereux.</em></p>
<p>Les agents de programmation IA comme Claude Code, OpenAI Codex et Google Gemini peuvent écrire du code, l’exécuter, lire les erreurs et réessayer. Tout repose sur cette boucle. Plus elle est rapide et informative, plus l’agent devient utile. Après avoir développé Curling IO version 3 en Gleam avec des agents de programmation IA, je suis convaincu que Gleam est le meilleur langage pour cette méthode de travail. Les agents n’écrivent pas un meilleur Gleam, car les données d’entraînement sont moins nombreuses. Mais son compilateur leur permet de se corriger sans attendre un humain.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-feedback-loop-that-matters">La boucle de rétroaction qui compte<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#the-feedback-loop-that-matters" class="hash-link" aria-label="Lien direct vers La boucle de rétroaction qui compte" title="Lien direct vers La boucle de rétroaction qui compte" translate="no">​</a></h2>
<p>Tous les agents de programmation IA fonctionnent de la même façon : écrire du code, vérifier qu’il fonctionne, corriger ce qui est brisé et recommencer. La qualité de l’étape de vérification détermine tout.</p>
<p>Dans un langage typé dynamiquement, vérifier que le code fonctionne signifie exécuter la suite de tests. Cela prend du temps. Les tests ne couvrent pas nécessairement ce qui est réellement brisé, et ils sont eux aussi du code : chaque test ajouté augmente la complexité du projet. Beaucoup de bogues ne se manifestent qu’à l’exécution, parfois bien plus tard en production. L’agent écrit du code qui semble correct. Vous le révisez, et il vous semble correct aussi. Puis un utilisateur rencontre une erreur nil à deux heures du matin.</p>
<p>Dans Gleam, vérifier signifie compiler. Cela prend quelques secondes. Quand la compilation échoue, les messages sont précis : voici le fichier, la ligne, ce que vous avez écrit et ce qui était attendu. L’agent les lit, corrige et compile à nouveau. Après quelques cycles, la structure du code est saine.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-the-compiler-catches">Ce que le compilateur détecte<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#what-the-compiler-catches" class="hash-link" aria-label="Lien direct vers Ce que le compilateur détecte" title="Lien direct vers Ce que le compilateur détecte" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="no-nulls">Aucune valeur nulle<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#no-nulls" class="hash-link" aria-label="Lien direct vers Aucune valeur nulle" title="Lien direct vers Aucune valeur nulle" translate="no">​</a></h3>
<p>Gleam n’a pas de valeur null. Les valeurs facultatives sont représentées par <code>Option(T)</code>, soit <code>Some(value)</code> ou <code>None</code>. Vous ne pouvez pas déréférencer une valeur nil par accident. Le compilateur oblige à traiter les deux cas. Les erreurs liées aux valeurs nulles sont <a href="https://www.harness.io/blog/10-exception-types-in-production-java-applications" target="_blank" rel="noopener noreferrer" class="">l’exception la plus fréquente en production</a>. Une étude portant sur plus d’un milliard d’événements les a trouvées dans 70 % des environnements de production.</p>
<p>Lorsqu’un agent écrit du JavaScript, il doit penser à vérifier les valeurs nulles partout. Il ne le fait pas toujours. Gleam élimine entièrement cette possibilité.</p>
<p>Les références nulles sont connues comme l’<a href="https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/" target="_blank" rel="noopener noreferrer" class="">erreur à un milliard de dollars</a>, et ce n’est pas qu’une préoccupation théorique. En juin 2025, <a href="https://dev.to/pantoai/how-a-null-pointer-exception-brought-down-mighty-google-7-hours-of-downtime-explained-p2g" target="_blank" rel="noopener noreferrer" class="">une seule valeur nulle dans un champ de base de données</a> s’est propagée dans le système Service Control de Google Cloud et a mis plusieurs produits GCP et Workspace hors service dans le monde pendant des heures. Cette valeur a atteint un parcours sans gestion d’erreur, s’est répliquée mondialement en quelques secondes et a causé une panne résolue complètement après près de trois heures. Le type <code>Option</code> de Gleam aurait obligé le développeur ou l’agent à traiter le cas manquant avant de compiler. Il n’empêche pas toutes les défaillances d’une telle chaîne, mais élimine la catégorie précise de bogue qui l’a déclenchée.</p>
<p>Il y a aussi un enjeu de sécurité. Des valeurs nulles non traitées peuvent laisser une application dans un état inattendu : vérifications d’authentification sautées parce qu’un nil s’est glissé dans le parcours, ou données exposées sur une page d’erreur qui n’aurait jamais dû être atteinte. Chaque catégorie d’erreur éliminée par le compilateur représente une surface d’attaque que l’agent ne peut pas introduire par accident.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="structural-changes">Changements structurels<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#structural-changes" class="hash-link" aria-label="Lien direct vers Changements structurels" title="Lien direct vers Changements structurels" translate="no">​</a></h3>
<p>Voici un exemple réel de notre code. Supposons que l’agent ajoute un champ à un type :</p>
<div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub type Listing {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  Listing(</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    id: Int,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    name: String,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    capacity: Int,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    waitlist_enabled: Bool,  // new field</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  )</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>En JavaScript, ajouter une propriété à un objet ne change rien avant l’exécution. Si un modèle ou un point d’accès API n’inclut pas le champ, vous ne le saurez que lorsque ce parcours sera exécuté. Les tests peuvent le détecter, ou le manquer si la couverture est incomplète.</p>
<p>En Gleam, toutes les fonctions qui construisent ou déstructurent un <code>Listing</code> cessent alors de compiler. Le compilateur liste chaque endroit à mettre à jour. L’agent suit la liste, fait les changements et obtient une compilation réussie.</p>
<p>Cela vaut pour tous les changements structurels : renommer un champ, passer un type de <code>String</code> à <code>Int</code>, ajouter une variante à un type union. Les agents s’en sortent bien. Les humains oublient des éléments dans les listes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="exhaustive-pattern-matching">Filtrage par motifs exhaustif<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#exhaustive-pattern-matching" class="hash-link" aria-label="Lien direct vers Filtrage par motifs exhaustif" title="Lien direct vers Filtrage par motifs exhaustif" translate="no">​</a></h3>
<p>Le compilateur Gleam exige que le filtrage par motifs couvre tous les cas possibles. Pour un <code>Result</code>, il faut traiter <code>Ok</code> et <code>Error</code>. Pour un type union personnalisé à quatre variantes, il faut traiter les quatre. Le compilateur vous signale tout oubli.</p>
<p>Prenons un type d’état de paiement :</p>
<div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub type PaymentStatus {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  Pending</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  Completed</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  Refunded</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  Failed</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Si quelqu’un ajoute une variante <code>Disputed</code>, toutes les expressions <code>case</code> qui filtrent un <code>PaymentStatus</code> dans le code cessent de compiler tant qu’elles ne traitent pas <code>Disputed</code>. Dans un langage typé dynamiquement, l’agent écrit un <code>switch</code> ou une chaîne de <code>if</code> pour les cas courants et oublie le cas limite. Une erreur d’exécution attend de se produire. En Gleam, le compilateur la détecte avant toute exécution.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="simple-syntax-fewer-ways-to-go-wrong">Une syntaxe simple, moins d’erreurs possibles<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#simple-syntax-fewer-ways-to-go-wrong" class="hash-link" aria-label="Lien direct vers Une syntaxe simple, moins d’erreurs possibles" title="Lien direct vers Une syntaxe simple, moins d’erreurs possibles" translate="no">​</a></h2>
<p>Gleam est un petit langage. Une seule façon de définir une fonction et de traiter les erreurs et les valeurs facultatives : <code>Result</code> et <code>Option</code>. Aucune exception, conversion implicite ou macro.</p>
<p>C’est important pour les agents, car un espace de décision réduit signifie moins de mauvaises décisions. Quand il existe six façons de faire quelque chose, l’agent doit choisir, et il ne choisit pas nécessairement l’approche idiomatique. En Gleam, il n’y en a généralement qu’une. L’agent n’a pas à connaître les préférences stylistiques de la communauté ou les conventions de gestion d’erreurs du projet. Le langage a déjà tranché.</p>
<p>Le formatage suit la même logique. <code>gleam format</code> fait autorité. Aucune configuration, aucun débat de style. Après formatage, le code de l’agent est identique en apparence au code écrit à la main. Impossible de voir la différence, et ce n’est pas nécessaire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="whitespace-doesnt-matter">Les espaces ne comptent pas<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#whitespace-doesnt-matter" class="hash-link" aria-label="Lien direct vers Les espaces ne comptent pas" title="Lien direct vers Les espaces ne comptent pas" translate="no">​</a></h2>
<p>Je préfère en fait les langages où les espaces sont significatifs. Elm et Haskell sont parmi mes préférés. Une indentation nette me semble plus belle que des accolades partout. Mais les agents ne la maîtrisent pas encore. D’après mon expérience, ils trébuchent constamment sur les modèles Slim. Les erreurs d’indentation sont fréquentes dans le code produit par les LLM. Le modèle peut mélanger tabulations et espaces ou se tromper d’un niveau d’imbrication. Ces erreurs sont silencieuses et sémantiques : elles changent ce que fait le code.</p>
<p>Gleam utilise des accolades. Les espaces ne comptent pas pour le compilateur. <code>gleam format</code> les normalise. Une catégorie d’erreurs en moins pour les agents.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="tight-feedback-loops">Des boucles de rétroaction rapides<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#tight-feedback-loops" class="hash-link" aria-label="Lien direct vers Des boucles de rétroaction rapides" title="Lien direct vers Des boucles de rétroaction rapides" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="fast-compilation">Une compilation rapide<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#fast-compilation" class="hash-link" aria-label="Lien direct vers Une compilation rapide" title="Lien direct vers Une compilation rapide" translate="no">​</a></h3>
<p>Le compilateur Gleam traite notre projet complet en quelques secondes. Une suite de tests typique dans un langage typé dynamiquement prend de 30 secondes à quelques minutes. Lorsque l’agent itère, l’écart entre quelques secondes de compilation et plusieurs minutes de tests s’accumule rapidement.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="fast-tests">Des tests rapides<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#fast-tests" class="hash-link" aria-label="Lien direct vers Des tests rapides" title="Lien direct vers Des tests rapides" translate="no">​</a></h3>
<p>Les tests sont rapides aussi. Chacun a sa propre base SQLite en mémoire sans état partagé, ce qui nous permet de <a class="" href="https://curling.io/fr/blog/parallel-tests-for-free">tous les exécuter en parallèle</a>. Environ 800 tests se terminent en moins d’une seconde. Ce gain s’accumule pendant une session où l’agent compile et teste des dizaines de fois.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="less-training-data-is-a-real-cost">Moins de données d’entraînement, un vrai coût<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#less-training-data-is-a-real-cost" class="hash-link" aria-label="Lien direct vers Moins de données d’entraînement, un vrai coût" title="Lien direct vers Moins de données d’entraînement, un vrai coût" translate="no">​</a></h2>
<p>Gleam a moins de données d’entraînement que JavaScript, TypeScript ou Python. Les agents produisent du code Gleam de moins bonne qualité au premier essai. Ils utilisent des approches inexistantes et inventent des fonctions absentes de la bibliothèque standard.</p>
<p>Le coût est réel : l’agent prend plus de temps à écrire le code initial en Gleam qu’en JavaScript.</p>
<p>Mais voici ce que j’ai constaté : le délai entre le début du travail et le déploiement d’un code correct est plus court en Gleam. L’agent écrit plus lentement, mais le compilateur détecte immédiatement les erreurs. En JavaScript, il écrit plus vite, mais les erreurs apparaissent plus tard, dans les tests ou en production.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-workflow-in-practice">La méthode en pratique<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#the-workflow-in-practice" class="hash-link" aria-label="Lien direct vers La méthode en pratique" title="Lien direct vers La méthode en pratique" translate="no">​</a></h2>
<p>Voici le déroulement d’une fonctionnalité typique :</p>
<ol>
<li class="">Nous écrivons une spécification de ce que nous voulons. C’est itératif : nous l’affinons avec l’agent avant d’écrire du code. Une bonne spécification fait la différence entre un résultat utile et du rebut.</li>
<li class="">L’agent écrit l’implémentation : types, requêtes de base de données, gestionnaires de requêtes et vues.</li>
<li class="">Il lance <code>gleam build</code>. La compilation échoue avec 5 à 10 erreurs.</li>
<li class="">L’agent lit chaque erreur, corrige et recompile. Il fait quelques cycles.</li>
<li class="">La compilation réussit. L’agent lance les tests. Ils passent ou échouent sur la logique métier, pas sur des erreurs de valeurs nulles ou de types incompatibles.</li>
<li class="">Nous révisons les différences, en regardant la logique, l’intention et la redondance.</li>
</ol>
<p>Les agents ont tendance à dupliquer du code plutôt qu’à réutiliser les fonctions existantes, probablement à cause des limites de leur fenêtre de contexte. Repérer les occasions d’extraire une logique partagée est notre commentaire le plus fréquent.</p>
<p>Nous écrivons encore beaucoup de code à la main. Les agents n’ont pas toujours raison. Ils produisent parfois du code laid ou redondant qu’il faut repérer et réécrire. Nous intervenons lorsque l’agent tourne en rond et essaie de corriger la même erreur sans avancer. En Gleam, cela arrive beaucoup moins que ce que nous avons vu dans les langages dynamiques.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="other-languages">Les autres langages<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#other-languages" class="hash-link" aria-label="Lien direct vers Les autres langages" title="Lien direct vers Les autres langages" translate="no">​</a></h2>
<p>D’autres langages à typage statique avec de bons compilateurs partagent certaines de ces propriétés. Rust, OCaml, Haskell et Elm ont tous des systèmes de types forts qui détectent les erreurs à la compilation.</p>
<p>Mais Gleam combine des caractéristiques qui conviennent particulièrement aux agents :</p>
<ul>
<li class="">Une syntaxe simple et facile à générer.</li>
<li class="">Une compilation rapide pour des boucles de rétroaction courtes.</li>
<li class="">Un formatage de référence qui donne au code de l’agent l’apparence du code écrit à la main.</li>
<li class="">Aucune valeur nulle, ce qui élimine une catégorie d’erreurs très courante.</li>
<li class="">Un filtrage par motifs exhaustif qui empêche d’oublier des cas.</li>
<li class="">Des espaces non significatifs, pour que les erreurs de formatage ne changent pas le comportement.</li>
</ul>
<p>Il fonctionne aussi sur BEAM, qui offre la tolérance aux pannes et la concurrence, mais c’est un <a class="" href="https://curling.io/fr/blog/background-jobs-without-the-baggage">autre sujet</a>.</p>
<p>Le compromis est la maturité de l’écosystème et les données d’entraînement. Gleam est jeune. Les bibliothèques sont moins nombreuses. Le code des agents demande plus de corrections au premier passage. Cet écart se réduit à mesure que les données d’entraînement augmentent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-bet">Le pari<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#the-bet" class="hash-link" aria-label="Lien direct vers Le pari" title="Lien direct vers Le pari" translate="no">​</a></h2>
<p>Je crois que les agents IA écrivent déjà la majorité du code dans un nombre croissant de projets. Le choix de langage est évalué autrement. La vitesse à laquelle un humain peut écrire compte moins. Le temps de révision exigé du développeur compte davantage que tout.</p>
<p>Le développeur est le goulot d’étranglement. Nous sommes lents comparés aux ordinateurs. Si le compilateur a déjà vérifié la structure du code avant l’arrivée des différences à l’écran, la personne qui révise peut se concentrer sur la logique et l’intention plutôt que de chercher les vérifications nil manquantes.</p>
<p>Nous avons choisi Gleam pour d’autres raisons, mais sa compatibilité avec les agents de programmation IA a été un avantage énorme.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="addendum-the-state-of-ai-assisted-coding">Addenda : où en est la programmation assistée par IA<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#addendum-the-state-of-ai-assisted-coding" class="hash-link" aria-label="Lien direct vers Addenda : où en est la programmation assistée par IA" title="Lien direct vers Addenda : où en est la programmation assistée par IA" translate="no">​</a></h2>
<p>Prenons du recul par rapport à l’argument technique. La programmation assistée par IA est là pour rester. Même si la technologie ne s’améliore pas sensiblement, elle est déjà assez utile pour que les développeurs l’adoptent en masse.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-bar-is-higher-not-lower">Les exigences augmentent<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#the-bar-is-higher-not-lower" class="hash-link" aria-label="Lien direct vers Les exigences augmentent" title="Lien direct vers Les exigences augmentent" translate="no">​</a></h3>
<p>Les agents sont déjà assez bons pour beaucoup de tâches d’applications métier, mais cette affirmation vient avec une grosse réserve. Chaque modification demande un examen attentif. Le développeur doit être un expert du domaine, de la sécurité et de la programmation. La vitesse de production du code a augmenté le niveau d’expertise requis du développeur plutôt que de le réduire.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="expect-more-security-vulnerabilities">Attendez-vous à plus de vulnérabilités<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#expect-more-security-vulnerabilities" class="hash-link" aria-label="Lien direct vers Attendez-vous à plus de vulnérabilités" title="Lien direct vers Attendez-vous à plus de vulnérabilités" translate="no">​</a></h3>
<p>Les agents produisent du code qui fonctionne, mais le fonctionnement et la sécurité sont des critères très différents. Ils détecteront probablement les problèmes évidents comme l’injection SQL. Les problèmes moins évidents sont dangereux : par exemple, ne pas réaliser qu’une décision prise trois demandes plus tôt rend indirectement les renseignements personnels de tous vos clients accessibles par une URL publique. Cela exige une compréhension de l’ensemble que les agents n’ont pas encore. Je pense que ce phénomène deviendra visible dans les prochaines années, à mesure que du code écrit par des agents arrivera en production sans révision suffisante.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-industry-is-evolving">Le secteur évolue<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#the-industry-is-evolving" class="hash-link" aria-label="Lien direct vers Le secteur évolue" title="Lien direct vers Le secteur évolue" translate="no">​</a></h3>
<p>Le secteur cherche activement les bonnes abstractions pour le développement assisté par IA. Deux tentatives se démarquent :</p>
<p><a href="https://moglang.org/" target="_blank" rel="noopener noreferrer" class="">Mog</a> est un langage conçu pour être écrit et lu par des IA plutôt que des humains. Je suis sceptique. Si le développeur qui révise les différences est le goulot d’étranglement, privilégier la lisibilité machine au détriment de la lisibilité humaine aggrave la situation.</p>
<p><a href="https://codespeak.dev/" target="_blank" rel="noopener noreferrer" class="">Codespeak</a> propose une autre approche : des spécifications et du code comme représentations interchangeables, avec un passage dans les deux sens. Je pense que les deux sont importants et qu’aucun ne suffit actuellement seul. Une spécification aide à réfléchir à l’ensemble, mais manque des détails qui comptent lorsque les choses tournent mal. Le code contient tous les détails, mais sa lecture permet difficilement de prendre du recul sur tout le système. Ils se situent à des niveaux de détail différents. Les fusionner en une seule chose fait perdre l’utilité propre à chacun.</p>
<p>Les langages à typage statique ont un avantage : les types se documentent eux-mêmes. Un type bien défini en Gleam communique déjà une bonne partie de ce qu’une spécification dirait de la structure des données et des limites d’une fonction, sans document distinct.</p>
<h3 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-comes-next">La suite<a href="https://curling.io/fr/blog/21-reasons-ai-agents-love-gleam#what-comes-next" class="hash-link" aria-label="Lien direct vers La suite" title="Lien direct vers La suite" translate="no">​</a></h3>
<p>Le rôle du développeur se déplace vers la supervision. Cela pourrait signifier beaucoup plus de logiciels créés, beaucoup moins de développeurs employés, ou les deux. Et ce rôle de supervision pourrait lui-même disparaître si les agents deviennent capables de boucler le travail seuls. C’est une possibilité réelle qu’il faut reconnaître honnêtement.</p>
<p>Après un an à développer des logiciels en production avec des agents de programmation IA, je constate que cela fonctionne si l’on fait l’effort de vérifier tout ce qui en sort. Gleam n’a pas été conçu pour les agents IA, mais une bonne conception du langage compte finalement plus que les outils créés expressément pour eux.</p>]]></content:encoded>
            <category>Gleam</category>
            <category>IA</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Les tests parallèles sans effort supplémentaire]]></title>
            <link>https://curling.io/fr/blog/parallel-tests-for-free</link>
            <guid>https://curling.io/fr/blog/parallel-tests-for-free</guid>
            <pubDate>Sun, 08 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. Pourquoi nous avons choisi Rust pour la version 3 explique cette décision et ce que nous avons conservé de cette exploration.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Mise à jour : 1er octobre 2026</div><div class="admonitionContent_Jlkn"><p>Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. <a class="" href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3">Pourquoi nous avons choisi Rust pour la version 3</a> explique cette décision et ce que nous avons conservé de cette exploration.</p></div></div>
<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cette note technique d’implémentation vient du développement de Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aborde Gleam, Erlang, EUnit, l’isolation des tests et l’exécution parallèle plus en détail que nos articles habituels sur le produit.</p></div></div>
<p>En écrivant l’<a class="" href="https://curling.io/fr/blog/sqlite-test-isolation">article précédent</a> sur nos bases SQLite par test, je décrivais comment chaque test reçoit sa propre base en mémoire, sans connexion ni état partagé. Une idée m’est venue : si rien n’est partagé, pouvons-nous simplement tous les exécuter en même temps?</p>
<p>Nous pouvions, et notre suite de tests serveur est passée d’environ 4 secondes à environ 0,85 seconde pour quelque 800 tests. Aucun changement dans les tests eux-mêmes. Un seul module Erlang de 25 lignes.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-sequential-was-the-default">Pourquoi l’exécution séquentielle était le choix par défaut<a href="https://curling.io/fr/blog/parallel-tests-for-free#why-sequential-was-the-default" class="hash-link" aria-label="Lien direct vers Pourquoi l’exécution séquentielle était le choix par défaut" title="Lien direct vers Pourquoi l’exécution séquentielle était le choix par défaut" translate="no">​</a></h2>
<p>L’exécuteur de tests de Gleam, gleeunit, délègue au cadriciel EUnit d’Erlang. Par défaut, EUnit exécute les modules de test un à la fois. C’est le choix prudent, car la plupart des suites partagent quelque part un état modifiable : une connexion à une base, un processus nommé ou un fichier sur disque. Exécuter ces tests simultanément produit des échecs qui passent localement mais échouent en intégration continue, ou qui passent le mardi et échouent le mercredi.</p>
<p>Nos tests ne partagent aucun état modifiable. Chaque test clone sa propre base. Deux tests ne touchent jamais la même connexion. L’article précédent explique toute la configuration, mais en résumé, <code>test_db.setup()</code> clone une base modèle en cache par l’API de sauvegarde SQLite et retourne une nouvelle connexion indépendante. À la fin du test, le ramasse-miettes récupère la connexion et la base disparaît.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="whats-actually-isolated">Ce qui est réellement isolé<a href="https://curling.io/fr/blog/parallel-tests-for-free#whats-actually-isolated" class="hash-link" aria-label="Lien direct vers Ce qui est réellement isolé" title="Lien direct vers Ce qui est réellement isolé" translate="no">​</a></h2>
<p>Avant d’activer le parallélisme, nous avons vérifié chaque catégorie d’état partagé :</p>
<table><thead><tr><th>Ressource</th><th>Isolation</th></tr></thead><tbody><tr><td>Bases SQLite</td><td>Chaque test clone sa propre base en mémoire</td></tr><tr><td>Cache <code>persistent_term</code></td><td>Initialisation idempotente, la première valeur écrite est conservée (base modèle, fuseaux horaires, configuration des journaux)</td></tr><tr><td>Tables ETS (limiteur de débit)</td><td>Sans nom, chaque test crée la sienne avec <code>ets:new</code></td></tr><tr><td>Processus nommés</td><td>Aucun démarré dans les tests</td></tr><tr><td>Système de fichiers</td><td>Aucune écriture dans les tests</td></tr></tbody></table>
<p>Les entrées <code>persistent_term</code> sont des caches écrits une seule fois. Plusieurs tests peuvent tenter d’initialiser la base modèle simultanément, mais la première initialisation est conservée, et les appels suivants ne font que lire la valeur en cache. C’est sûr.</p>
<p>Les tables ETS des tests, pour le limiteur de débit, sont créées sans l’option <code>named_table</code>. Chaque appel à <code>ets:new</code> retourne donc une référence de table unique. Aucun conflit.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-implementation">L’implémentation<a href="https://curling.io/fr/blog/parallel-tests-for-free#the-implementation" class="hash-link" aria-label="Lien direct vers L’implémentation" title="Lien direct vers L’implémentation" translate="no">​</a></h2>
<p>EUnit prend en charge l’enveloppe <code>{inparallel, Tests}</code>, qui distribue les fonctions de test entre les ordonnanceurs BEAM. Elle existe depuis des années, mais gleeunit ne l’expose pas. Nous avons proposé une demande de fusion pour ajouter une fonction <code>main_parallel()</code>. On nous a fait remarquer que cela convenait mieux à une solution propre au projet qu’à une modification du cœur de gleeunit. C’est logique. Nous avons fermé la demande et écrit un module Erlang local au projet qui reproduit la découverte des tests et appelle EUnit directement.</p>
<div class="language-erlang codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-erlang codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">-module(parallel_test_runner).</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">-export([main/0]).</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">main() -&gt;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    Options = [verbose, no_tty,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">               {report, {gleeunit_progress, [{colored, true}]}},</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">               {scale_timeouts, 10}],</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    Files = filelib:wildcard("**/*.{erl,gleam}", "test"),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    Modules = [to_module(list_to_binary(F)) || F &lt;- Files],</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    case eunit:test({inparallel, Modules}, Options) of</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        ok -&gt; erlang:halt(0);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        _  -&gt; erlang:halt(1)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    end.</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">to_module(Path) -&gt;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    case filename:extension(Path) of</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        &lt;&lt;".gleam"&gt;&gt; -&gt;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            binary_to_atom(</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              binary:replace(</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                binary:replace(Path, &lt;&lt;".gleam"&gt;&gt;, &lt;&lt;""&gt;&gt;),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                &lt;&lt;"/"&gt;&gt;, &lt;&lt;"@"&gt;&gt;, [global]),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              utf8);</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        &lt;&lt;".erl"&gt;&gt; -&gt;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            binary_to_atom(</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              binary:replace(lists:last(binary:split(Path, &lt;&lt;"/"&gt;&gt;, [global])),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                &lt;&lt;".erl"&gt;&gt;, &lt;&lt;""&gt;&gt;),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              utf8)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    end.</span><br></div></code></pre></div></div>
<p>Ce module fait la même chose que gleeunit : chercher les fichiers de test avec un motif glob et convertir leurs noms en atomes de module. Il enveloppe toutefois la liste dans <code>{inparallel, ...}</code> plutôt que de la passer à plat. Il réutilise <code>gleeunit_progress</code> de la dépendance Hex pour afficher un point coloré par test. La sortie des tests reste donc la même.</p>
<p>Le point d’entrée tient en une ligne :</p>
<div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">@external(erlang, "parallel_test_runner", "main")</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub fn main() -&gt; Nil</span><br></div></code></pre></div></div>
<p>Aucun fork, aucun sous-module. Le gleeunit standard reste une dépendance dans <code>gleam.toml</code>, car nous l’utilisons encore pour l’affichage de progression. Seul le paquet serveur utilise l’exécuteur parallèle. Les paquets partagé et client gardent le <code>main()</code> séquentiel standard de gleeunit, car ils ont beaucoup moins de tests et n’en ont pas besoin.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-results">Les résultats<a href="https://curling.io/fr/blog/parallel-tests-for-free#the-results" class="hash-link" aria-label="Lien direct vers Les résultats" title="Lien direct vers Les résultats" translate="no">​</a></h2>
<table><thead><tr><th>Paquet</th><th>Tests</th><th>Avant</th><th>Après</th></tr></thead><tbody><tr><td>server</td><td>environ 800</td><td>environ 4,0 s</td><td>environ 0,85 s</td></tr><tr><td>shared</td><td>environ 100</td><td>environ 0,5 s</td><td>environ 0,5 s</td></tr><tr><td>client</td><td>1</td><td>environ 0,2 s</td><td>environ 0,2 s</td></tr><tr><td><strong>Total</strong></td><td><strong>environ 900</strong></td><td><strong>environ 4,7 s</strong></td><td><strong>environ 1,6 s</strong></td></tr></tbody></table>
<p>Les paquets partagé et client n’ont pas changé, car ils s’exécutaient déjà en moins d’une seconde.</p>
<p>EUnit sérialise les rappels de son écouteur dans un seul processus. Les points de progression arrivent donc toujours dans l’ordre. La sortie standard de chaque test est livrée après sa fin. Les sorties des tests simultanés ne s’entremêlent donc pas. La sortie des tests est identique à celle d’avant, simplement plus rapide.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="keeping-it-working">Garder cette approche fonctionnelle<a href="https://curling.io/fr/blog/parallel-tests-for-free#keeping-it-working" class="hash-link" aria-label="Lien direct vers Garder cette approche fonctionnelle" title="Lien direct vers Garder cette approche fonctionnelle" translate="no">​</a></h2>
<p>Cela fonctionne parce que l’architecture des tests est saine. Si quelqu’un ajoute un test qui enregistre un processus nommé avec un atome fixe ou écrit dans un chemin de fichier partagé, deux copies de ce test entreront en conflit et produiront des échecs instables. Les règles sont simples :</p>
<ul>
<li class="">Ne partagez pas de connexions entre les tests. Utilisez <code>test_db.setup()</code>.</li>
<li class="">N’enregistrez pas les processus avec des noms fixes. Si un processus nommé est nécessaire, incluez le nom du module de test.</li>
<li class="">N’utilisez pas de tables ETS nommées. Transmettez plutôt la référence de table.</li>
<li class="">N’écrivez pas dans le système de fichiers pendant les tests.</li>
</ul>
<p>Si un test ne peut pas suivre ces règles, EUnit permet de mélanger <code>{inparallel, ...}</code> et <code>{inorder, ...}</code> dans une même exécution. Nous pourrions conserver une liste de modules séquentiels dans l’exécuteur et les regrouper séparément :</p>
<div class="language-erlang codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-erlang codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">Sequential = [some_integration_test],</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Parallel = Modules -- Sequential,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">eunit:test([{inparallel, Parallel}, {inorder, Sequential}], Options)</span><br></div></code></pre></div></div>
<p>Les modules séquentiels s’exécuteraient un à la fois pendant que les autres resteraient parallèles. Nous n’en avons pas encore eu besoin, mais quelques lignes suffiraient.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-accidental-win">Le gain inattendu<a href="https://curling.io/fr/blog/parallel-tests-for-free#the-accidental-win" class="hash-link" aria-label="Lien direct vers Le gain inattendu" title="Lien direct vers Le gain inattendu" translate="no">​</a></h2>
<p>Rien de cela n’était prévu. Nous avions choisi des bases SQLite par test pour éliminer le nettoyage, les dépendances d’ordre et les tests instables causés par des fuites d’état. Nous sommes encore un peu surpris qu’une accélération permanente de 4,7 fois, dont nous profiterons pendant toute la vie du projet, ait demandé un après-midi et 25 lignes d’Erlang.</p>
<p>Trente-sept lignes au total, avec les 12 lignes du module FFI de l’article précédent, pour isoler les bases par test et exécuter les tests en parallèle sur tous les cœurs disponibles. Aucune extension de cadriciel de test, aucune configuration. Le tout se compile avec <code>gleam build</code> et s’exécute avec <code>gleam test</code>.</p>
<hr>
<p><em>Cet article est le septième de la série sur les fondations de Curling IO. À suivre : <a class="" href="https://curling.io/fr/blog/live-admin-without-javascript">un panneau d’administration en direct sans écrire de JavaScript</a>.</em></p>]]></content:encoded>
            <category>Fondations</category>
            <category>Gleam</category>
            <category>BEAM</category>
            <category>Tests</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Toutes les associations provinciales et territoriales de curling du Canada utilisent Curling IO]]></title>
            <link>https://curling.io/fr/blog/curling-io-powers-provincial-associations</link>
            <guid>https://curling.io/fr/blog/curling-io-powers-provincial-associations</guid>
            <pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Les 14 associations provinciales et territoriales de curling utilisent Curling IO. Voici ce que cela signifie pour les clubs et les curleurs.]]></description>
            <content:encoded><![CDATA[<p>Toutes les associations provinciales et territoriales de curling du Canada utilisent Curling IO. Ce sont 14 associations membres sur la même plateforme, avec leurs clubs affiliés. Curling IO prend aussi en charge les inscriptions et la gestion des compétitions au niveau national.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-full-list">La liste complète<a href="https://curling.io/fr/blog/curling-io-powers-provincial-associations#the-full-list" class="hash-link" aria-label="Lien direct vers La liste complète" title="Lien direct vers La liste complète" translate="no">​</a></h2>
<ul>
<li class="">
<a href="https://ab.curling.io/" target="_blank">Curling Alberta</a>
</li>
<li class="">
<a href="https://bc.curling.io/" target="_blank">Curl BC</a>
</li>
<li class="">
<a href="https://mb.curling.io/" target="_blank">CurlManitoba</a>
</li>
<li class="">
<a href="https://sk.curling.io/" target="_blank">CURLSASK</a>
</li>
<li class="">
<a href="https://on.curling.io/" target="_blank">Curling Ontario</a>
</li>
<li class="">
<a href="https://noca.curling.io/" target="_blank">Northern Ontario Curling Association</a>
</li>
<li class="">
<a href="https://qc.curling.io/" target="_blank">Curling Québec</a>
</li>
<li class="">
<a href="https://nb.curling.io/" target="_blank">New Brunswick Curling Association</a>
</li>
<li class="">
<a href="https://ns.curling.io/" target="_blank">Nova Scotia Curling</a>
</li>
<li class="">
<a href="https://nl.curling.io/" target="_blank">Newfoundland &amp; Labrador Curling Association</a>
</li>
<li class="">
<a href="https://pe.curling.io/" target="_blank">Curl PEI</a>
</li>
<li class="">
<a href="https://nt.curling.io/" target="_blank">Curling NT</a>
</li>
<li class="">
<a href="https://nu.curling.io/" target="_blank">Nunavut Curling Association</a>
</li>
<li class="">
<a href="https://yt.curling.io/" target="_blank">Yukon Curling Association</a>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="what-associations-use-it-for">Ce que les associations en font<a href="https://curling.io/fr/blog/curling-io-powers-provincial-associations#what-associations-use-it-for" class="hash-link" aria-label="Lien direct vers Ce que les associations en font" title="Lien direct vers Ce que les associations en font" translate="no">​</a></h2>
<p>Les associations provinciales utilisent Curling IO pour leurs compétitions : championnats provinciaux, séries de qualification et qualifications régionales. Elles gèrent les inscriptions des équipes, planifient les séances, saisissent les pointages en direct et publient les résultats. Les partisans et les participants voient les tableaux, les classements et les tableaux de pointage se mettre à jour en temps réel.</p>
<p>Les associations perçoivent aussi les frais d’affiliation par la plateforme. Lorsqu’un curleur s’inscrit à son club, les frais d’inscription du club et les frais d’affiliation de l’association peuvent être perçus en une seule transaction. Les frais de l’association et ceux du club apparaissent au même passage à la caisse. Aucune facturation distincte, aucun rapprochement manuel.</p>
<p>Le lien avec les frais d’affiliation est facultatif. Les clubs peuvent utiliser Curling IO indépendamment sans devoir appliquer les frais de l’association ni lui transmettre les données de leurs membres. Pour les clubs qui choisissent ce lien, la perception des frais et la production des rapports sont automatisées.</p>
<p>La plateforme est entièrement bilingue, en anglais et en français. C’est important pour des associations comme Curling Québec et pour les clubs de partout au pays qui comptent des membres francophones.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-it-matters-for-clubs">Ce que cela apporte aux clubs<a href="https://curling.io/fr/blog/curling-io-powers-provincial-associations#why-it-matters-for-clubs" class="hash-link" aria-label="Lien direct vers Ce que cela apporte aux clubs" title="Lien direct vers Ce que cela apporte aux clubs" translate="no">​</a></h2>
<p>Lorsque votre association provinciale utilise Curling IO, vous avez quelques avantages.</p>
<p><strong>Vos curleurs ont déjà des profils.</strong> Un curleur qui s’est inscrit à un bonspiel provincial ou a joué dans un championnat existe déjà dans le système. Lorsqu’il s’inscrit à votre club, il retrouve son profil. Pas besoin de saisir de nouveau son nom, son adresse ou ses contacts d’urgence.</p>
<p><strong>Les compétitions sont liées.</strong> Les résultats d’un curleur dans les ligues de club, les qualifications provinciales et les championnats nationaux sont dans le même système. Son profil le suit des ligues de club aux compétitions provinciales et nationales.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-it-matters-for-curlers">Ce que cela apporte aux curleurs<a href="https://curling.io/fr/blog/curling-io-powers-provincial-associations#why-it-matters-for-curlers" class="hash-link" aria-label="Lien direct vers Ce que cela apporte aux curleurs" title="Lien direct vers Ce que cela apporte aux curleurs" translate="no">​</a></h2>
<p>Pour un curleur, Curling IO est un seul compte. Il s’inscrit à sa ligue du mardi soir au club local, à un bonspiel de fin de semaine dans un autre club, puis se qualifie pour un championnat provincial. Même connexion, même profil, même parcours de paiement.</p>
<p>Le système compte actuellement environ 150 000 profils de curleurs dans des centaines de clubs actifs et les 14 associations membres provinciales et territoriales. Ce nombre augmente chaque saison avec l’arrivée de nouveaux clubs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="getting-started">Pour commencer<a href="https://curling.io/fr/blog/curling-io-powers-provincial-associations#getting-started" class="hash-link" aria-label="Lien direct vers Pour commencer" title="Lien direct vers Pour commencer" translate="no">​</a></h2>
<p>Si votre club relève d’une association provinciale canadienne, celle-ci utilise déjà Curling IO. Vos frais d’affiliation et votre base de données de curleurs sont donc liés dès le premier jour. Il n’y a aucuns frais de configuration, aucuns frais mensuels et aucun contrat.</p>
<p>Les clubs hors du Canada peuvent aussi utiliser Curling IO, sans le lien avec l’association.</p>
<p>Consultez notre <a class="" href="https://curling.io/fr/docs/getting-started/curling-club-managers">guide de démarrage</a> ou la page Curling IO de votre association provinciale pour voir le fonctionnement.</p>]]></content:encoded>
            <category>Associations provinciales</category>
            <category>Base de données des curleurs</category>
        </item>
        <item>
            <title><![CDATA[L’isolation des tests offerte par SQLite]]></title>
            <link>https://curling.io/fr/blog/sqlite-test-isolation</link>
            <guid>https://curling.io/fr/blog/sqlite-test-isolation</guid>
            <pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. Pourquoi nous avons choisi Rust pour la version 3 explique cette décision et ce que nous avons conservé de cette exploration.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Mise à jour : 1er octobre 2026</div><div class="admonitionContent_Jlkn"><p>Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. <a class="" href="https://curling.io/fr/blog/why-we-chose-rust-for-version-3">Pourquoi nous avons choisi Rust pour la version 3</a> explique cette décision et ce que nous avons conservé de cette exploration.</p></div></div>
<div class="theme-admonition theme-admonition-note admonition_HXej alert alert--secondary"><div class="admonitionHeading_XmGX"><span class="admonitionIcon_dj3L"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>À propos de cet article</div><div class="admonitionContent_Jlkn"><p>Cette note technique d’implémentation porte sur l’architecture des tests de Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aborde SQLite, les bases de données en mémoire, l’API de sauvegarde et l’isolation des tests plus en détail que nos articles habituels sur le produit.</p></div></div>
<p>Les tests de Curling IO n’ont besoin ni d’une base de données de test partagée, ni de fonctions de nettoyage, ni d’astuces transactionnelles. Chaque test a sa propre base de données. Il peut donc réussir seul ou dans la suite complète pour la même raison : rien d’autre ne peut toucher ses données.</p>
<p>Cela découle d’un choix de la version 3 : SQLite fonctionne dans le processus de l’application. Chaque test obtient une base SQLite en mémoire complètement indépendante, clonée depuis un modèle en quelques microsecondes avec l’API de sauvegarde de SQLite.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-shared-database-problem">Le problème de la base de données partagée<a href="https://curling.io/fr/blog/sqlite-test-isolation#the-shared-database-problem" class="hash-link" aria-label="Lien direct vers Le problème de la base de données partagée" title="Lien direct vers Le problème de la base de données partagée" translate="no">​</a></h2>
<p>Dans une configuration de test Postgres ou MySQL traditionnelle, chaque test communique avec le même serveur de base de données. Il faut une stratégie pour empêcher les tests de se contaminer :</p>
<p><strong>Annulation de transaction.</strong> Encapsuler chaque test dans une transaction, puis l’annuler à la fin. Cela fonctionne jusqu’à ce que le code utilise ses propres transactions ou crée des processus qui doivent voir les données du test, le problème classique du bac à sable Ecto.</p>
<p><strong>Vidage des tables.</strong> Supprimer toutes les lignes de toutes les tables entre les tests. C’est lent, et il faut respecter le bon ordre pour éviter les violations de clés étrangères.</p>
<p><strong>Outil de nettoyage de base de données.</strong> Une gem ou bibliothèque qui combine les deux stratégies avec une configuration des tables à nettoyer, de la stratégie et du moment du nettoyage. Cela fonctionne, mais cette mécanique existe uniquement à cause de la base partagée.</p>
<p>Ce sont toutes des solutions de contournement à la même contrainte architecturale : un serveur de base de données, plusieurs tests.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="in-memory-sqlite-one-database-per-test">SQLite en mémoire : une base de données par test<a href="https://curling.io/fr/blog/sqlite-test-isolation#in-memory-sqlite-one-database-per-test" class="hash-link" aria-label="Lien direct vers SQLite en mémoire : une base de données par test" title="Lien direct vers SQLite en mémoire : une base de données par test" translate="no">​</a></h2>
<p>SQLite ouvre une base de données à partir d’un chemin de fichier. Passez plutôt <code>:memory:</code> pour obtenir une base en mémoire qui existe seulement pendant la durée de la connexion. Elle est rapide, sans entrées-sorties disque, et complètement isolée. Rien d’autre ne peut la voir, et fermer la connexion libère tout.</p>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Exemple de configuration de base</summary><div><div class="collapsibleContent_ZBPi"><p>Notre fonction de préparation des tests :</p><div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub fn setup() -&gt; sqlight.Connection {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(conn) = sqlight.open(":memory:")</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(_) = sqlight.exec("PRAGMA foreign_keys=ON;", conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(_) = sqlight.exec(schema_sql, conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  conn</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div><p>La variable <code>schema_sql</code> contient le contenu de <code>db/schema.sql</code>, régénéré à partir des migrations par <code>bin/reset</code>. Les tests utilisent donc toujours le schéma actuel sans que quelqu’un doive penser à les mettre à jour.</p><p>Chaque test appelle <code>setup()</code>, reçoit une nouvelle base avec le schéma complet, insère les données nécessaires et exécute ses assertions. À la fin du test, le ramasse-miettes récupère la connexion et la base disparaît. Il n’y a rien à nettoyer.</p><div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub fn should_reject_duplicate_registration_test() {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let conn = test_db.setup()</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  // Insert test-specific data</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  insert_org(conn, test_org())</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  insert_listing(conn, test_listing())</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  // ... test logic, assertions</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  // conn goes out of scope, database vanishes</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div></div></div></details>
<p>Aucune contamination croisée n’est possible, car rien n’est partagé. Les tests A et B utilisent littéralement des bases différentes. Vous pouvez les exécuter en parallèle dans des processus BEAM sans coordination.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-schema-execution-cost">Le coût de l’exécution du schéma<a href="https://curling.io/fr/blog/sqlite-test-isolation#the-schema-execution-cost" class="hash-link" aria-label="Lien direct vers Le coût de l’exécution du schéma" title="Lien direct vers Le coût de l’exécution du schéma" translate="no">​</a></h2>
<p>Cette approche a un coût évident : chaque test exécute tout le schéma. Chez nous, cela représente 77 instructions <code>CREATE TABLE</code>, <code>CREATE INDEX</code> et <code>CREATE VIEW</code>. Nous l’avons mesuré :</p>
<ul>
<li class="">Ouverture d’une base SQLite en mémoire : <strong>15 microsecondes</strong>.</li>
<li class="">Exécution de 77 instructions de schéma : <strong>1 100 microsecondes</strong>, soit 1,1 ms.</li>
</ul>
<p>À 1,1 ms par test pour environ 500 tests qui ont besoin d’une base, cela donne environ 550 ms d’exécution du schéma. Ce n’est pas terrible, mais pas gratuit non plus. Le coût augmente linéairement avec le nombre de tests et la taille du schéma.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="sqlites-backup-api-page-level-cloning">L’API de sauvegarde de SQLite : cloner les pages<a href="https://curling.io/fr/blog/sqlite-test-isolation#sqlites-backup-api-page-level-cloning" class="hash-link" aria-label="Lien direct vers L’API de sauvegarde de SQLite : cloner les pages" title="Lien direct vers L’API de sauvegarde de SQLite : cloner les pages" translate="no">​</a></h2>
<p>SQLite possède une <a href="https://www.sqlite.org/backup.html" target="_blank" rel="noopener noreferrer" class="">API de sauvegarde</a> conçue pour copier des bases entre des connexions. Elle travaille au niveau des pages : elle copie les pages brutes de la base d’une connexion à l’autre sans analyser ou exécuter de nouveau le SQL.</p>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Implémentation du clonage du modèle</summary><div><div class="collapsibleContent_ZBPi"><p>L’idée : construire le schéma une seule fois dans une base modèle, puis la cloner pour chaque test.</p><div class="language-text codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-text codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">Once at startup:  open(":memory:") → execute full schema → template</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Per test:         open(":memory:") → backup_copy(template) → ready</span><br></div></code></pre></div></div><p>L’API de sauvegarde demande trois appels : <code>backup_init</code> pour préparer la copie, <code>backup_step</code> avec <code>-1</code> pour copier toutes les pages d’un coup, puis <code>backup_finish</code> pour libérer les ressources.</p><p>Notre pilote SQLite Gleam, <a href="https://hexdocs.pm/sqlight/" target="_blank" rel="noopener noreferrer" class="">sqlight</a>, n’expose pas cette API. Il repose toutefois sur <a href="https://github.com/mmzeeman/esqlite" target="_blank" rel="noopener noreferrer" class="">esqlite</a>, une enveloppe NIF Erlang de la bibliothèque C de SQLite, qui expose toute l’API de sauvegarde. Puisque Gleam se compile en Erlang et fonctionne sur BEAM, son interface de fonctions étrangères (FFI) permet d’appeler directement les bibliothèques Erlang. Nous avons écrit un module Erlang de 12 lignes qui prend deux connexions sqlight, en réalité des enregistrements esqlite, et effectue le clonage :</p><div class="language-erlang codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-erlang codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">-module(test_db_ffi).</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">-export([clone_db/1]).</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">clone_db(Template) -&gt;</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    {ok, Dest} = esqlite3:open(":memory:"),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    {ok, Backup} = esqlite3:backup_init(Dest, "main", Template, "main"),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    '$done' = esqlite3:backup_step(Backup, -1),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    ok = esqlite3:backup_finish(Backup),</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    {ok, Dest}.</span><br></div></code></pre></div></div><p>Côté Gleam, nous déclarons la liaison FFI et l’appelons comme une fonction ordinaire :</p><div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">@external(erlang, "test_db_ffi", "clone_db")</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">fn clone_db(</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  template: sqlight.Connection,</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">) -&gt; Result(sqlight.Connection, Nil)</span><br></div></code></pre></div></div><p>C’est un avantage de l’écosystème BEAM. Lorsque le pilote de haut niveau n’expose pas ce qu’il vous faut, la bibliothèque de bas niveau le fait presque toujours, et il est très simple de franchir la frontière FFI.</p><p>Le modèle est créé une seule fois par exécution de la suite de tests et mis en cache dans <code>persistent_term</code> de BEAM, un stockage global immuable optimisé pour les accès surtout en lecture :</p><div class="language-gleam codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-gleam codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token plain">fn template_db() -&gt; sqlight.Connection {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  case get_cached("test_template_db") {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    Ok(conn) -&gt; conn</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    Error(Nil) -&gt; {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      let assert Ok(conn) = sqlight.open(":memory:")</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      let assert Ok(_) = sqlight.exec("PRAGMA foreign_keys=ON;", conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      let assert Ok(_) = sqlight.exec(schema_sql, conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      cache("test_template_db", conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      conn</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">pub fn setup() -&gt; sqlight.Connection {</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(conn) = clone_db(template_db())</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  let assert Ok(_) = sqlight.exec("PRAGMA foreign_keys=ON;", conn)</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  conn</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div><p>Le <code>PRAGMA foreign_keys=ON</code> est exécuté après le clonage, car les paramètres PRAGMA de SQLite sont propres à la connexion et ne sont pas stockés dans le fichier de base de données. Une seule instruction, au coût négligeable.</p></div></div></details>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-numbers">Les chiffres<a href="https://curling.io/fr/blog/sqlite-test-isolation#the-numbers" class="hash-link" aria-label="Lien direct vers Les chiffres" title="Lien direct vers Les chiffres" translate="no">​</a></h2>
<p>Au passage à la méthode de clonage, nous avions 594 tests et 77 objets de schéma. Nous sommes encore au début de la réécriture et approchons déjà les 1 000 tests. D’ici le lancement, nous prévoyons près du double d’objets de schéma et environ 5 000 tests.</p>
<p>Voici les mesures à 594 tests :</p>
<table><thead><tr><th>Approche</th><th>Durée des tests serveur</th></tr></thead><tbody><tr><td>Exécuter le schéma complet par test</td><td>3,2 s</td></tr><tr><td>Cloner le modèle par l’API de sauvegarde</td><td>2,6 s</td></tr></tbody></table>
<p>Le coût d’exécution du schéma est passé d’environ 550 ms à presque zéro. Les 2,6 secondes restantes correspondent à la logique réelle des tests, soit l’insertion de données, l’exécution de la logique métier et les assertions, ainsi qu’au démarrage de la machine virtuelle BEAM.</p>
<p>Les économies deviennent plus importantes à grande échelle. Une configuration Postgres traditionnelle viderait les tables entre les tests avec un <code>TRUNCATE TABLE</code> sur chaque table, dans un ordre respectant les clés étrangères, par un socket vers le serveur. Avec environ 100 tables et 5 000 tests, une estimation prudente pour notre réécriture complète, qui pourrait réalistement compter deux à trois fois plus de tests, cela représente 500 000 instructions de vidage envoyées au serveur. Même à moins d’une milliseconde par instruction, le total s’accumule.</p>
<p>Avec la sauvegarde, le clonage coûte environ 25 microsecondes par test, peu importe la taille du schéma. Pour 5 000 tests, cela donne 0,125 seconde au total. Aucun ordre de vidage à respecter, aucun aller-retour par socket.</p>
<p>Il s’agit d’une vraie suite de tests applicatifs couvrant les opérations de panier, le traitement des paiements, la validation des inscriptions, les frais d’association, les renonciations, la génération de tournois à la ronde, la planification des horaires, les pointages et plus encore. Ce ne sont pas des tests triviaux.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="why-this-works-so-well">Pourquoi cela fonctionne si bien<a href="https://curling.io/fr/blog/sqlite-test-isolation#why-this-works-so-well" class="hash-link" aria-label="Lien direct vers Pourquoi cela fonctionne si bien" title="Lien direct vers Pourquoi cela fonctionne si bien" translate="no">​</a></h2>
<p>L’API de sauvegarde est une bonne optimisation, mais le principal gain vient de SQLite dans le processus, qui élimine complètement le problème de la base partagée.</p>
<p>Avec Postgres, le processus de test communique avec un serveur de base de données par un socket. Chaque test qui écrit des données le fait au même endroit. L’architecture exige donc des stratégies d’isolation.</p>
<p>Avec SQLite dans le processus, la base vit dans la mémoire de ce processus. En créer une autre est une allocation mémoire, pas une connexion réseau. Aucun serveur avec lequel se coordonner, aucun pool de connexions à gérer. L’isolation découle gratuitement de l’architecture.</p>
<p>Cela signifie aussi :</p>
<ul>
<li class="">Aucune dépendance à l’ordre des tests. Chaque test est entièrement autonome.</li>
<li class="">Un parallélisme sûr. Chaque processus BEAM reçoit sa propre base sans coordination.</li>
<li class="">Aucun code de nettoyage : ni <code>teardown</code>, ni <code>after_each</code>, ni <code>database_cleaner</code>.</li>
<li class="">Aucun test instable causé par un état partagé qui fuit entre les exécutions.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="trade-offs">Les compromis<a href="https://curling.io/fr/blog/sqlite-test-isolation#trade-offs" class="hash-link" aria-label="Lien direct vers Les compromis" title="Lien direct vers Les compromis" translate="no">​</a></h2>
<p>Il reste des compromis. Vous ne testez pas avec le même moteur de base de données qu’en production, sauf si vous utilisez aussi SQLite en production, comme nous. Des tests SQLite pour une production Postgres manqueraient les comportements propres à Postgres : types personnalisés, verrous consultatifs, opérateurs jsonb, etc.</p>
<p>Dans notre configuration, chaque test construit aussi ses données à partir de zéro. En pratique, c’est un avantage, car il déclare explicitement ses dépendances, mais cela demande plus de code de préparation par test qu’un jeu de données partagé. C’est toutefois un choix, pas une limite de la technique. Vous pourriez tout aussi bien insérer des données de départ dans la base modèle avant de la mettre en cache. Chaque clone commencerait avec ces données. Si votre suite profite d’un ensemble bien défini d’utilisateurs, d’organisations ou d’autres données de référence, préparer le modèle ainsi réduit simplement la préparation par test tout en conservant une isolation complète.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="this-isnt-beam-specific">Au-delà de BEAM<a href="https://curling.io/fr/blog/sqlite-test-isolation#this-isnt-beam-specific" class="hash-link" aria-label="Lien direct vers Au-delà de BEAM" title="Lien direct vers Au-delà de BEAM" translate="no">​</a></h2>
<p>Nos exemples utilisent Gleam et Erlang, mais l’API de sauvegarde est une fonction de SQLite au niveau C. La plupart des langages l’exposent :</p>
<details class="details_G9Mc alert alert--info details_zJUW" data-collapsed="true"><summary>Exemples en Python, Node.js et Rust</summary><div><div class="collapsibleContent_ZBPi"><div class="language-python codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-python codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># Python 3.7+ has it built in</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">source </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> sqlite3</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">":memory:"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">source</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">executescript</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">schema_sql</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">dest </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> sqlite3</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">":memory:"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">source</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">backup</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">dest</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div><div class="language-javascript codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-javascript codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// Node.js better-sqlite3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> template </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">new</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Database</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">":memory:"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">template</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token method function property-access" style="color:rgb(130, 170, 255)">exec</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">schemaSQL</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> dest </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> template</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token method function property-access" style="color:rgb(130, 170, 255)">backup</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">":memory:"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><br></div></code></pre></div></div><div class="language-rust codeBlockContainer_KwUm theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_uftZ"><pre tabindex="0" class="prism-code language-rust codeBlock_tsfc thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_MKtf"><div class="token-line" style="color:#bfc7d5"><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// Rust rusqlite</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">let</span><span class="token plain"> template </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Connection</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token function" style="color:rgb(130, 170, 255)">open_in_memory</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">template</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">execute_batch</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">schema_sql</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">let</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">mut</span><span class="token plain"> dest </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Connection</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token function" style="color:rgb(130, 170, 255)">open_in_memory</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">let</span><span class="token plain"> backup </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token namespace" style="color:rgb(178, 204, 214)">backup</span><span class="token namespace punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token class-name" style="color:rgb(255, 203, 107)">Backup</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token function" style="color:rgb(130, 170, 255)">new</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token operator" style="color:rgb(137, 221, 255)">&amp;</span><span class="token plain">template</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">&amp;</span><span class="token keyword" style="font-style:italic">mut</span><span class="token plain"> dest</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">backup</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">run_to_completion</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token number" style="color:rgb(247, 140, 108)">5</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Duration</span><span class="token punctuation" style="color:rgb(199, 146, 234)">::</span><span class="token constant" style="color:rgb(130, 170, 255)">ZERO</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">None</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token operator" style="color:rgb(137, 221, 255)">?</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><br></div></code></pre></div></div></div></div></details>
<p>Le modèle est le même dans chaque langage : construire un modèle une fois, puis le cloner par test avec l’API de sauvegarde. Notre détour FFI par esqlite est nécessaire seulement parce que notre pilote Gleam ne l’expose pas encore.</p>
<h2 class="anchor anchorTargetStickyNavbar_Kbpc" id="the-full-stack">L’ensemble de l’infrastructure<a href="https://curling.io/fr/blog/sqlite-test-isolation#the-full-stack" class="hash-link" aria-label="Lien direct vers L’ensemble de l’infrastructure" title="Lien direct vers L’ensemble de l’infrastructure" translate="no">​</a></h2>
<p>Voici toute notre infrastructure de test :</p>
<ul>
<li class=""><code>test_db.setup()</code> clone une base modèle en cache avec l’API de sauvegarde SQLite.</li>
<li class=""><code>test_db.tz_db()</code> retourne une base de fuseaux horaires en cache, chargée une seule fois plutôt que par test.</li>
<li class=""><code>persistent_term</code> est le cache global BEAM du modèle de base et des données de fuseaux horaires.</li>
<li class="">Douze lignes de FFI Erlang enveloppent l’API de sauvegarde SQLite.</li>
</ul>
<p>C’est toute l’infrastructure de test. Aucune extension de cadriciel de test, aucune gem de nettoyage de base, aucune stratégie de vidage. SQLite et 12 lignes d’Erlang.</p>
<hr>
<p><em>Cet article est le sixième de la série sur les fondations de Curling IO. À suivre : <a class="" href="https://curling.io/fr/blog/parallel-tests-for-free">les tests parallèles sans effort supplémentaire</a>.</em></p>]]></content:encoded>
            <category>Fondations</category>
            <category>SQLite</category>
            <category>Tests</category>
            <category>Architecture</category>
        </item>
    </channel>
</rss>