<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
    <title>BlackMesa Labs</title>
    <subtitle>Notes d&#039;ingénierie : ce qu&#039;on a construit, ce qu&#039;on a cassé, ce qu&#039;on en a tiré.</subtitle>
    <link href="https://mesa.black/feed.xml" rel="self"/>
    <link href="https://mesa.black/"/>
    <updated>2026-10-04T00:00:00+00:00</updated>
    <id>https://mesa.black/</id>
        <entry>
        <title>Notre couche Domaine ne contient que des exceptions. C&#039;est délibéré.</title>
        <link href="https://mesa.black/notre-domaine-ne-contient-que-des-exceptions/"/>
        <id>https://mesa.black/notre-domaine-ne-contient-que-des-exceptions/</id>
        <updated>2026-10-04T00:00:00+00:00</updated>
        <summary>Neuf contextes, vingt-neuf commandes, un seul gestionnaire de requête, aucun port. Ce qu&#039;on a gardé de DDD et de l&#039;architecture hexagonale, ce qu&#039;on a refusé, et les cinq endroits où l&#039;infrastructure traverse la frontière quand même. Ce n&#039;est pas du DDD puriste, c&#039;est du time to market.</summary>        <content type="html">&lt;p&gt;Les articles sur l’architecture hexagonale montrent presque toujours le même schéma : un cercle au milieu, des ports tout autour, des adaptateurs à l’extérieur, et une flèche qui rentre. Aucun ne montre ce que contient le dossier &lt;code&gt;Domain/&lt;/code&gt; six mois après.&lt;/p&gt;
&lt;p&gt;Voici le nôtre. Neuf contextes, et au total douze fichiers de domaine : neuf exceptions, deux énumérations, un rôle. Zéro interface. Zéro agrégat. Zéro objet-valeur. Appeler ça une couche métier serait un mensonge, et c’est précisément le sujet de cet article.&lt;/p&gt;
&lt;h2&gt;Ce qu’on a gardé : le C de CQRS, pas le Q&lt;/h2&gt;
&lt;p&gt;La configuration du bus déclare trois canaux, avec une sémantique explicite :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;default_bus: command.bus
buses:
    command.bus:            # une commande mute l&#039;état, exactement un gestionnaire
        default_middleware: { enabled: true, allow_no_handlers: false }
    query.bus:              # une requête renvoie une valeur, un seul gestionnaire
        default_middleware: { enabled: true, allow_no_handlers: false }
    event.bus:              # un événement de domaine : 0..n abonnés
        default_middleware: { enabled: true, allow_no_handlers: true }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le décompte réel, aujourd’hui : &lt;strong&gt;vingt-neuf gestionnaires de commande, un seul gestionnaire de requête.&lt;/strong&gt; Le bus de requêtes existe, il est configuré, il est presque vide.&lt;/p&gt;
&lt;p&gt;Ce n’est pas un retard de migration, c’est une conclusion. Une commande vaut sa cérémonie parce qu’elle apporte trois choses qu’on n’avait pas : un nom d’intention (&lt;code&gt;ChangeCompanySubscriptionPlan&lt;/code&gt; n’est pas &lt;code&gt;setSubscriptionPlan&lt;/code&gt;), la garantie qu’il existe exactement un endroit qui l’exécute — &lt;code&gt;allow_no_handlers: false&lt;/code&gt; échoue au démarrage, pas en production — et une frontière de transaction évidente, celle du gestionnaire.&lt;/p&gt;
&lt;p&gt;Une lecture n’apporte rien de tout ça. Sa forme est dictée par l’écran qui l’affiche : cette page a besoin de ces sept colonnes, jointes de cette façon, triées comme ça. Faire passer ça par un bus n’ajoute aucune règle, ajoute une couche, et déplace le SQL d’un fichier à un autre. On lit donc par les dépôts Doctrine, directement, sans s’en excuser.&lt;/p&gt;
&lt;p&gt;Détail qui compte plus qu’il n’en a l’air : &lt;code&gt;default_bus: command.bus&lt;/code&gt;. Un &lt;code&gt;dispatch()&lt;/code&gt; nu est une commande. La valeur par défaut est celle qui mute l’état, donc la seule qu’on ne veut jamais voir partir par accident sur le mauvais canal.&lt;/p&gt;
&lt;h2&gt;Ce qu’on a refusé : l’inversion de dépendance&lt;/h2&gt;
&lt;p&gt;C’est le cœur de l’hexagone dans la littérature : le domaine définit des interfaces, l’infrastructure les implémente, la flèche de dépendance pointe vers l’intérieur. On ne l’a pas fait. Voici un gestionnaire entier, sans coupe :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;#[AsMessageHandler(bus: &#039;command.bus&#039;)]
final readonly class ChangeCompanySubscriptionPlanHandler
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private LoggerInterface $logger,
    ) {}

    public function __invoke(ChangeCompanySubscriptionPlanCommand $command): void
    {
        $company = $this-&amp;gt;entityManager-&amp;gt;find(Company::class, Ulid::fromString($command-&amp;gt;companyId));
        if (!$company instanceof Company) {
            throw CompanyNotFoundException::withId($command-&amp;gt;companyId);
        }
        // …
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;EntityManagerInterface&lt;/code&gt; en dépendance directe. Sur trente-huit gestionnaires, &lt;strong&gt;vingt-quatre&lt;/strong&gt; en dépendent. L’entité &lt;code&gt;Company&lt;/code&gt; vient de &lt;code&gt;App\Entity&lt;/code&gt;, partagée par tous les contextes. Il n’y a pas de dépôt abstrait, pas de port, pas de modèle de domaine distinct de la table.&lt;/p&gt;
&lt;p&gt;Deux remarques sur cet exemple, parce qu’elles disent plus que le schéma.&lt;/p&gt;
&lt;p&gt;D’abord, la commande transporte &lt;code&gt;string $companyId&lt;/code&gt;, pas un objet-valeur &lt;code&gt;CompanyId&lt;/code&gt;. Ce n’est pas de la paresse : le message doit pouvoir être sérialisé pour le transport asynchrone. La frontière du bus impose la forme du message — de l’infrastructure qui dicte la signature de ce qu’on appelle le domaine, dès la première ligne.&lt;/p&gt;
&lt;p&gt;Ensuite, &lt;code&gt;Ulid::fromString(...)&lt;/code&gt;. L’identifiant du domaine existe en deux formes selon qu’on le transporte ou qu’on l’interroge, et la conversion est une contrainte du stockage, pas du métier.&lt;/p&gt;
&lt;h2&gt;Ce que la frontière achète quand même&lt;/h2&gt;
&lt;p&gt;Une chose, une seule, et elle vaut son prix : &lt;strong&gt;le couplage entre contextes est devenu visible dans la liste des imports.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Le gestionnaire ci-dessus vit dans &lt;code&gt;App\Billing&lt;/code&gt;. Il importe &lt;code&gt;App\Company\Domain\Exception\CompanyNotFoundException&lt;/code&gt;. Ce seul import dit que la facturation dépend du contexte Entreprise, et il le dit en haut du fichier, en une ligne, sans diagramme à tenir à jour. Le graphe de dépendances entre contextes s’obtient avec un &lt;code&gt;grep&lt;/code&gt; sur les &lt;code&gt;use&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;C’est modeste. Comparé à une base où tout vit dans &lt;code&gt;App\Service&lt;/code&gt;, c’est la différence entre « on suppose que c’est couplé » et « voilà exactement où ». C’est ce bénéfice-là qu’on a acheté, et rien d’autre.&lt;/p&gt;
&lt;h2&gt;Les cinq endroits où l’infrastructure traverse&lt;/h2&gt;
&lt;p&gt;Voilà ce qu’aucun schéma ne montre : les endroits où Doctrine décide de la forme d’une opération métier. Ce sont des fuites réelles, toutes présentes dans le code aujourd’hui.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. La règle métier écrite en deux dialectes.&lt;/strong&gt; Notre politique d’exclusion des entreprises internes des chiffres commerciaux tient en un prédicat. Elle existe en deux versions, parce que certains chemins de lecture sont en SQL natif pour la performance et d’autres en DQL :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public static function sql(string $alias = &#039;c&#039;): string  // requêtes natives
{ return ($alias !== &#039;&#039; ? $alias.&#039;.&#039; : &#039;&#039;).&#039;excluded_from_stats = false&#039;; }

public static function dql(string $alias = &#039;c&#039;): string  // requêtes de l&#039;ORM
{ return $alias.&#039;.excludedFromStats = false&#039;; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Une règle, deux écritures, à cause du langage de requête. La divergence est d’un caractère — &lt;code&gt;excluded_from_stats&lt;/code&gt; contre &lt;code&gt;excludedFromStats&lt;/code&gt; — donc invisible à la relecture rapide, et il n’existe aucun test qui échouerait si l’une des deux partait à la dérive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Le filtre ambiant.&lt;/strong&gt; Le filtre de suppression logique de Doctrine est un état global mutable. Une requête qui cherche un identifiant unique ne trouve rien alors que l’index unique, lui, le retient toujours. On s’en sort en désactivant le filtre, puis en le remettant :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$filters = $this-&amp;gt;em-&amp;gt;getFilters();
$wasEnabled = $filters-&amp;gt;isEnabled(&#039;softdeleteable&#039;);
if ($wasEnabled) { $filters-&amp;gt;disable(&#039;softdeleteable&#039;); }
$existing = $repo-&amp;gt;findOneBy([&#039;slug&#039; =&amp;gt; $slug]);
if ($wasEnabled) { $filters-&amp;gt;enable(&#039;softdeleteable&#039;); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Une « requête métier » dont le résultat dépend d’un état ambiant n’est pas une fonction. Ce n’est pas un défaut de Doctrine — le filtre est exactement ce qu’on lui demande — mais ça interdit de raisonner sur le code applicatif sans savoir dans quel mode il s’exécute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. L’ordre des écritures dans l’unité de travail.&lt;/strong&gt; Remplacer les traductions d’un contenu semble être une opération : on retire les anciennes, on ajoute les nouvelles, on enregistre. Dans un seul &lt;code&gt;flush()&lt;/code&gt;, Doctrine exécute les &lt;code&gt;INSERT&lt;/code&gt; avant les &lt;code&gt;DELETE&lt;/code&gt; d’orphelins, et la contrainte d’unicité sur &lt;code&gt;(feedback_id, locale)&lt;/code&gt; casse. L’opération doit être coupée en deux enregistrements successifs.&lt;/p&gt;
&lt;p&gt;Autrement dit : la découpe de l’opération applicative n’est pas dictée par le métier mais par l’ordonnancement interne de l’ORM. Aucun port n’aurait protégé de ça, parce que la contrainte ne vit ni dans le domaine ni dans l’adaptateur — elle vit dans le calendrier d’exécution qui les relie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. L’alias de jointure qui tronque en silence.&lt;/strong&gt; Filtrer sur une association déjà jointe en &lt;code&gt;fetch&lt;/code&gt; restreint aussi la collection hydratée : on croit filtrer les lignes, on ampute l’objet retourné. Il faut une seconde jointure dédiée au filtrage :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$qb-&amp;gt;innerJoin(&#039;f.industries&#039;, &#039;i_filter&#039;)      // alias distinct de celui du fetch
   -&amp;gt;andWhere(&#039;i_filter.id IN (:industries)&#039;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le bug ne se voit pas : la requête renvoie les bonnes entités, avec des collections incomplètes. C’est de la sémantique d’infrastructure, au beau milieu de ce qui ressemble à une règle de recherche.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Le type de l’identifiant.&lt;/strong&gt; Les identifiants circulent en ULID, se comparent en RFC 4122 dans les requêtes, et l’oubli de la conversion produit un résultat vide plutôt qu’une erreur. Un port aurait déplacé la conversion ; il ne l’aurait pas supprimée.&lt;/p&gt;
&lt;h2&gt;La règle de migration&lt;/h2&gt;
&lt;p&gt;Reste la question qui tue les refontes : que fait-on du code existant, écrit en services Symfony classiques ?&lt;/p&gt;
&lt;p&gt;Rien, tant que personne ne le touche. La politique est écrite : &lt;strong&gt;CQRS pour les nouvelles écritures et les nouvelles lectures ; migration de l’existant à la demande uniquement.&lt;/strong&gt; Jamais de réécriture en masse au nom de l’homogénéité.&lt;/p&gt;
&lt;p&gt;Parce que l’homogénéité n’est pas un résultat métier. Réécrire un service qui fonctionne pour qu’il ressemble à ses voisins, c’est produire du risque sans produire de valeur — et c’est exactement le genre de travail qui se justifie tout seul, indéfiniment, parce que son critère d’arrêt est esthétique.&lt;/p&gt;
&lt;h2&gt;Ce n’est pas du DDD puriste, c’est du time to market&lt;/h2&gt;
&lt;p&gt;Il faut nommer la vraie raison, parce que tout ce qui précède se lit autrement une fois qu’elle est posée.&lt;/p&gt;
&lt;p&gt;On a déjà écrit ici que &lt;a href=&quot;/le-code-qu-on-n-ecrit-pas/&quot;&gt;la fonctionnalité la moins chère est celle qu’on ne construit pas&lt;/a&gt;. Un port est une fonctionnalité. Un agrégat aussi, un objet-valeur aussi, une couche anticorruption aussi. Chacun a un coût d’écriture, un coût de lecture pour celui qui arrivera après, et un coût d’entretien qui court aussi longtemps que le code vit. Une abstraction n’est pas gratuite parce qu’elle est immatérielle.&lt;/p&gt;
&lt;p&gt;La question utile n’est donc pas « est-ce du DDD correct ». C’est : qu’est-ce que cette abstraction achète aujourd’hui, et qu’est-ce qu’elle se contente de repousser ? Un dépôt derrière une interface achète la possibilité de changer de stockage — nous ne quitterons pas PostgreSQL. Il achète aussi des tests sans base de données, et ça, c’est un vrai bénéfice : c’est le seul argument qui nous fera peut-être payer la facture un jour.&lt;/p&gt;
&lt;p&gt;Ce qu’on a fait à la place tient en une phrase : livrer, et ne garder que les frontières qui se paient toutes seules. Le bus de commandes coûte trois fichiers et rend service le jour même. Les ports coûtent une couche entière et rendront peut-être service, plus tard, à une équipe qui n’existe pas encore.&lt;/p&gt;
&lt;p&gt;Le risque de cette position est connu, et l’écrire fait partie du prix : elle ressemble à s’y méprendre à de la paresse. La différence tient à un seul détail — on sait nommer ce qu’on n’a pas construit, et pourquoi.&lt;/p&gt;
&lt;p&gt;Et on assume. Ces choix sont les nôtres, pas des accidents qu’on découvrirait en relisant. On les corrige quand on peut et quand on a le temps : le jour où une gêne devient réelle et mesurable, pas le jour où un article d’architecture nous explique qu’ils sont incorrects. Une dette écrite, datée et argumentée n’est pas une dette niée — c’est la seule qu’on soit capable de rembourser au bon moment.&lt;/p&gt;
&lt;h2&gt;Ce que ça a donné&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Neuf contextes nommés&lt;/strong&gt;, dont le couplage mutuel se lit dans les imports plutôt que dans un diagramme périmé.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vingt-neuf commandes&lt;/strong&gt;, chacune avec un nom d’intention, un gestionnaire unique garanti au démarrage et une frontière de transaction évidente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un seul gestionnaire de requête&lt;/strong&gt;, assumé : les lectures passent par les dépôts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Douze fichiers de domaine&lt;/strong&gt; — neuf exceptions, deux énumérations, un rôle — c’est-à-dire aucune règle métier réellement isolée de Doctrine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zéro réécriture&lt;/strong&gt; du code classique existant, qui continue de fonctionner à côté.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bonnes pratiques&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Traiter chaque abstraction comme une fonctionnalité : elle doit dire ce qu’elle achète aujourd’hui, pas ce qu’elle permettrait un jour.&lt;/li&gt;
&lt;li&gt;Adopter les morceaux séparément. Le bus de commandes apporte quelque chose sans le reste ; les ports, les agrégats et les objets-valeurs sont des achats distincts, avec chacun leur facture.&lt;/li&gt;
&lt;li&gt;Choisir la valeur par défaut la plus contraignante : le bus par défaut est celui qui mute l’état, et l’absence de gestionnaire fait échouer le démarrage plutôt que la production.&lt;/li&gt;
&lt;li&gt;Se servir des espaces de noms comme d’un révélateur de couplage plutôt que comme d’une barrière : ils ne protègent de rien, ils rendent visible.&lt;/li&gt;
&lt;li&gt;Écrire la règle de migration, avec son critère d’arrêt. Sans critère d’arrêt, « harmoniser l’architecture » est un travail infini.&lt;/li&gt;
&lt;li&gt;Regarder ce que contient réellement &lt;code&gt;Domain/&lt;/code&gt; avant de dire qu’on fait du DDD. Le décompte est instructif.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Points de vigilance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Le dossier n’est pas la frontière.&lt;/strong&gt; Créer &lt;code&gt;Domain/&lt;/code&gt;, &lt;code&gt;Application/&lt;/code&gt;, &lt;code&gt;Infrastructure/&lt;/code&gt; donne une impression d’avancement mesurable — et mesurer une architecture au nombre de dossiers est la meilleure façon de n’obtenir aucune des garanties qu’ils suggèrent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vingt-quatre gestionnaires sur trente-huit dépendent de l’&lt;code&gt;EntityManager&lt;/code&gt;&lt;/strong&gt; : aucun ne peut être testé avec un dépôt en mémoire. La suite de tests a besoin d’une vraie base. C’est un coût, il est assumé, il n’est pas gratuit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Les entités Doctrine partagées sont le vrai couplage&lt;/strong&gt;, et il est invisible dans l’arborescence. Le jour où deux contextes voudront faire diverger la même table, c’est ce mur-là qu’on rencontrera, pas l’absence de ports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une règle métier dupliquée dans deux dialectes de requête finira par diverger&lt;/strong&gt;, et aujourd’hui rien ne le détecterait. C’est la dette la plus concrète de tout ce qui précède.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
        <entry>
        <title>Nous étions notre propre meilleur client. C&#039;était un bug.</title>
        <link href="https://mesa.black/notre-propre-meilleur-client/"/>
        <id>https://mesa.black/notre-propre-meilleur-client/</id>
        <updated>2026-09-30T00:00:00+00:00</updated>
        <summary>Notre entreprise publie sur notre plateforme — et se comptait dans notre chiffre d&#039;affaires, notre funnel et nos listes de relance. Comment on l&#039;a sortie des statistiques sans la sortir du site.</summary>        <content type="html">&lt;p&gt;On recommande à tout le monde d’utiliser son propre produit. Personne ne prévient de l’effet secondaire : à partir du moment où votre entreprise a un compte, elle entre dans vos chiffres. Pas dans une note de bas de page — dans le chiffre d’affaires, dans le taux de conversion, dans les listes de clients à relancer.&lt;/p&gt;
&lt;p&gt;Notre cas : BlackMesa publie de vrais retours d’expérience sur &lt;a href=&quot;https://showmetherex.com/&quot;&gt;Show me the REX&lt;/a&gt;. Le compte est réel, le plan est réel, les articles sont lus. Ce qui n’est pas réel, c’est le revenu : on ne se facture pas. Résultat, un client à zéro euro figurait dans le MRR, gonflait la répartition par plan, et remplissait une case du funnel de conversion sans avoir jamais rien converti.&lt;/p&gt;
&lt;p&gt;La correction évidente — « on exclut notre entreprise » — est fausse. Elle suppose que le problème est l’entreprise. Le problème est ailleurs.&lt;/p&gt;
&lt;h2&gt;Ce n’est pas « qui exclure », c’est « qu’est-ce que ce chiffre répond »&lt;/h2&gt;
&lt;p&gt;Le réflexe est de chercher une liste d’entités à écarter. La bonne question se pose métrique par métrique : à quelle question ce chiffre sert-il de réponse ?&lt;/p&gt;
&lt;p&gt;Un compteur de vues répond à « combien de personnes ont lu ce texte ». La réponse est la même que le lecteur vienne de chez nous ou d’ailleurs : la page a été servie, elle a été lue, le contenu existe. Exclure nos vues rendrait ce chiffre faux.&lt;/p&gt;
&lt;p&gt;Un chiffre d’affaires mensuel répond à « combien nous paient nos clients ». Notre propre entreprise n’est pas un client. Sa présence rend ce chiffre faux.&lt;/p&gt;
&lt;p&gt;Les deux métriques regardent la même base de données, parfois la même ligne, et ont besoin de règles opposées. Ce n’est pas une exception à gérer, c’est la règle : &lt;strong&gt;l’unité d’exclusion n’est pas la donnée, c’est la question posée.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Le cas qui tranche : une vue qui compte et ne compte pas&lt;/h2&gt;
&lt;p&gt;Le meilleur exemple est aussi le plus inconfortable, parce qu’il interdit tout raccourci d’implémentation.&lt;/p&gt;
&lt;p&gt;Sur la plateforme, une vue de REX alimente deux choses distinctes. D’un côté l’audience : le compteur affiché sur l’article, le cumul du tableau de bord, la courbe de fréquentation. De l’autre un signal commercial : « quelles entreprises ont consulté vos REX », qui sert à identifier des contacts à relancer.&lt;/p&gt;
&lt;p&gt;Même événement, même enregistrement. Dans le premier groupe, une lecture faite depuis chez nous compte — quelqu’un a vraiment lu. Dans le second, elle ne doit surtout pas compter : nous ne sommes pas un prospect à rappeler, et un lecteur interne dans une liste de leads, c’est une action commerciale déclenchée pour rien.&lt;/p&gt;
&lt;p&gt;Il n’existe donc pas de filtre global qu’on poserait une fois à l’entrée. Chaque requête doit savoir à quelle famille elle appartient.&lt;/p&gt;
&lt;h2&gt;Un drapeau en base, pas une constante dans le code&lt;/h2&gt;
&lt;p&gt;Première version, la plus rapide : une liste de noms d’entreprises en dur. Elle a tenu une heure. Une constante oblige à un déploiement pour reclasser une entreprise, et surtout elle ment sur la nature de l’information : « cette entreprise n’est pas un client » est une donnée métier, qui change, qui se décide, et qui doit être visible par la personne qui administre les comptes.&lt;/p&gt;
&lt;p&gt;C’est donc devenu une case à cocher sur la fiche entreprise — avec son texte d’aide écrit noir sur blanc, parce qu’une option dont personne ne comprend la portée finit cochée au hasard :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Nos propres entreprises publient et restent visibles en front, mais ne comptent dans aucune donnée commerciale ou marketing : MRR, plans, funnel, upsell/churn, leads, récaps. Les vues et visites, elles, restent comptées.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La liste de noms n’a pas disparu pour autant : elle sert d’amorçage, pour qu’un environnement fraîchement installé ne démarre jamais avec des chiffres pollués. Mais elle ne décide plus rien.&lt;/p&gt;
&lt;p&gt;Le prédicat lui-même vit dans une seule petite classe, en deux versions — une pour les requêtes de l’ORM, une pour le SQL brut. Ça paraît anecdotique ; c’est ce qui rend la règle auditable. Chercher les endroits qui l’appliquent est devenu une recherche de texte, et la comparaison avec la liste des chiffres commerciaux se fait à l’œil en une minute.&lt;/p&gt;
&lt;h2&gt;Vérifier plutôt que relire&lt;/h2&gt;
&lt;p&gt;Relire son propre code d’exclusion ne prouve rien : on relit ce qu’on croit avoir écrit. La seule vérification qui vaille consiste à basculer la case et à comparer les tableaux de bord avant et après, chiffre par chiffre.&lt;/p&gt;
&lt;p&gt;Une trentaine de valeurs se sont déplacées. Sept sont restées rigoureusement identiques — et c’était le résultat attendu : ce sont les compteurs d’audience et les volumes de contenu publié. Un chiffre qui bouge alors qu’il ne devait pas est un bug ; un chiffre qui ne bouge pas alors qu’il aurait dû est un oubli. Sans ce passage, on n’aurait distingué ni l’un ni l’autre.&lt;/p&gt;
&lt;p&gt;Au final : un drapeau, une classe de politique, trente-quatre prédicats répartis sur huit fichiers — MRR, plans, funnel, upsell, risque de départ, relances, intelligence commerciale, récapitulatifs mensuels, expiration des codes promo, nurturing.&lt;/p&gt;
&lt;p&gt;Ce dernier mérite une mention. Sans l’exclusion, notre propre entreprise entrait dans la séquence de relance automatique. On se serait envoyé nos propres e-mails de réengagement.&lt;/p&gt;
&lt;h2&gt;Ce que ça a donné&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Un MRR qui ne contient plus de client à zéro euro&lt;/strong&gt;, et un taux de conversion qui ne compte plus notre propre inscription dans son dénominateur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une trentaine de valeurs corrigées&lt;/strong&gt; sur les tableaux de bord commerciaux, sept volontairement inchangées.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zéro changement en front&lt;/strong&gt; : les REX restent publiés, visibles, filtrables, et leurs vues comptent toujours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une règle écrite à un seul endroit&lt;/strong&gt;, avec la raison, plutôt qu’un &lt;code&gt;AND&lt;/code&gt; recopié de requête en requête.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une case à cocher&lt;/strong&gt; qui permet de reclasser une entreprise sans déployer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bonnes pratiques&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Classer chaque métrique avant de coder le filtre : mesure d’audience, ou signal commercial. La réponse détermine la règle, et elle n’est pas la même pour deux chiffres issus de la même table.&lt;/li&gt;
&lt;li&gt;Mettre l’appartenance en base, pas dans une constante : c’est une donnée métier, elle change et elle se décide.&lt;/li&gt;
&lt;li&gt;Concentrer le prédicat dans une classe unique, même s’il tient en une ligne : ce qui compte n’est pas la réutilisation, c’est de pouvoir retrouver tous les appels.&lt;/li&gt;
&lt;li&gt;Vérifier en basculant : comparer les chiffres avant et après est le seul moyen de distinguer un oubli d’une décision.&lt;/li&gt;
&lt;li&gt;Écrire le périmètre dans l’interface d’administration, à l’endroit où la case se coche.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Points de vigilance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rien n’empêche la prochaine requête d’oublier le prédicat.&lt;/strong&gt; La règle est tenue par la relecture, pas par un test, et c’est aujourd’hui la faiblesse principale du dispositif.&lt;/li&gt;
&lt;li&gt;Une exclusion trop large est aussi fausse qu’une exclusion absente : retirer nos vues du compteur d’audience aurait produit un chiffre mensonger dans l’autre sens.&lt;/li&gt;
&lt;li&gt;Le jour où une entreprise interne devient un vrai client payant, la case doit se décocher — et personne ne le rappellera. La décision est humaine, elle a besoin d’un moment où on la reprend.&lt;/li&gt;
&lt;li&gt;Dire publiquement que vos statistiques excluent vos propres données ne coûte rien, et vaut mieux que de le découvrir à la place de quelqu’un d’autre.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
        <entry>
        <title>La fonctionnalité la moins chère est celle qu&#039;on ne construit pas</title>
        <link href="https://mesa.black/le-code-qu-on-n-ecrit-pas/"/>
        <id>https://mesa.black/le-code-qu-on-n-ecrit-pas/</id>
        <updated>2026-09-27T00:00:00+00:00</updated>
        <summary>Trois signaux qui disent « n&#039;écris pas ce code », et ce qu&#039;ils nous ont fait économiser en une seule journée.</summary>        <content type="html">&lt;p&gt;La sobriété numérique se pose presque toujours comme un problème d’optimisation : des
images plus légères, un meilleur cache, une région plus propre. Tout est vrai, et tout est
marginal. Le vrai gaspillage est ailleurs, et personne ne le mesure : &lt;strong&gt;le code écrit pour
rien&lt;/strong&gt;. Une migration à refaire, un outil acheté puis abandonné, une fonctionnalité livrée
que personne n’ouvre. Reste la question qu’on n’avait jamais formulée : comment &lt;em&gt;décide&lt;/em&gt;-t-on
de ne pas construire quelque chose ?&lt;/p&gt;
&lt;p&gt;Le REX donne les &lt;strong&gt;trois signaux&lt;/strong&gt; qu’on s’est donnés, et ce que chacun a économisé sur une
journée de travail ordinaire : une fonctionnalité cassée dont personne ne s’est plaint —
information sur la fonctionnalité, pas sur le bug ; 1 400 traductions qu’on n’a pas écrites
parce qu’aucun lecteur ne pouvait les atteindre ; et un manque de supervision qui s’est
révélé être un manque de répétition, pas un manque d’outil.&lt;/p&gt;
&lt;p&gt;Il dit aussi ce que ces signaux &lt;strong&gt;ne disent pas&lt;/strong&gt;. « Personne n’en a besoin » est l’excuse
parfaite pour ne rien faire, et une règle qui ne sait dire que non a cessé d’être une règle.
Deux garde-fous la gardent honnête — dont celui-ci : une fonctionnalité inutilisée est
parfois mal exposée plutôt qu’inutile.&lt;/p&gt;
</content>
    </entry>
        <entry>
        <title>Monter l&#039;OS sous Docker : ce qui reste couplé, et ce que rien ne valide</title>
        <link href="https://mesa.black/monter-l-os-sous-docker/"/>
        <id>https://mesa.black/monter-l-os-sous-docker/</id>
        <updated>2026-09-27T00:00:00+00:00</updated>
        <summary>L&#039;application vit dans une image, donc la distribution ne peut rien lui faire. Restent quatre points de couplage — et l&#039;un d&#039;eux se trouve dans un angle mort qu&#039;aucune chaîne d&#039;intégration ne couvre.</summary>        <content type="html">&lt;p&gt;&lt;a href=&quot;https://showmetherex.com/&quot;&gt;Show me the REX&lt;/a&gt; tourne sur un hôte unique : un Postgres, un Redis, un proxy frontal, et l’application déployée en blue-green — deux instances identiques, « bleue » et « verte », dont une seule sert le trafic à la fois. La nouvelle version est démarrée sur celle qui dort, et quand elle répond correctement, on bascule le proxy dessus. Au moment où nous écrivons, cet hôte sert 52 REX publiés et un peu plus de 16 000 vues cumulées.&lt;/p&gt;
&lt;p&gt;Ce dispositif protège une livraison : si la nouvelle version se comporte mal, rebasculer prend une seconde et personne ne s’en aperçoit. Il ne protège rien du tout contre la machine elle-même, puisque les deux instances vivent sur ce même hôte — arrêtez-le, vous les arrêtez toutes les deux.&lt;/p&gt;
&lt;p&gt;L’hôte unique est un choix assumé, pas une négligence : pas de redondance tant que le trafic ne la justifie pas, et le seuil de révision est écrit — mille visites par jour. En dessous, une seconde machine coûte bien plus en complexité (réplication, bascule, cohérence) qu’elle ne rapporte en disponibilité, et la complexité dont personne n’a encore besoin est la chose la plus chère qu’on puisse construire.&lt;/p&gt;
&lt;p&gt;Assumer ce choix crée une obligation, en revanche. Si on accepte qu’un redémarrage coupe le service, on se doit d’avoir le chiffre exact de ce que ça coûte. C’est précisément là qu’on s’est plantés cette nuit-là, et on y revient à la fin.&lt;/p&gt;
&lt;p&gt;Nous avons fait passer cet hôte d’une version d’Ubuntu à la suivante. L’opération est passée, et ce n’est pas l’intéressant. L’intéressant, c’est que la conteneurisation a réduit ce qu’une telle montée peut encore casser à une liste très courte — et que nous y avons trouvé précisément l’élément que rien, dans notre chaîne, ne surveillait.&lt;/p&gt;
&lt;h2&gt;Ce que la conteneurisation a réellement découplé&lt;/h2&gt;
&lt;p&gt;L’application est immunisée contre une montée de version, et il vaut la peine d’être précis sur le pourquoi : elle n’utilise rien de l’hôte. Son PHP, ses extensions, ses bibliothèques système, son magasin de certificats voyagent dans l’image. On peut remplacer intégralement le &lt;code&gt;ca-certificates&lt;/code&gt; de la machine : nos appels sortants vers VIES, le prestataire de paiement et le stockage objet n’en savent rien, parce qu’ils ne lisent jamais ce magasin.&lt;/p&gt;
&lt;p&gt;C’est pour ça que les équipes en sont venues à traiter une montée d’OS sur un hôte de conteneurs comme une routine. C’est presque vrai, et c’est ce « presque » qui coûte.&lt;/p&gt;
&lt;h2&gt;Les quatre points de couplage qui restent&lt;/h2&gt;
&lt;p&gt;Une fois qu’on a mis en image tout ce qui pouvait l’être, ce qui appartient encore à l’hôte tient en une liste courte — et c’est la même sur n’importe quel serveur conteneurisé :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Le noyau.&lt;/strong&gt; Les conteneurs le partagent. Un saut majeur change le sol sous une base de données bien davantage que sous un processus web.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Le démon Docker.&lt;/strong&gt; Il n’est pas dans votre image : c’est un paquet de l’hôte, installé depuis un dépôt tiers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Les sources apt de ce démon.&lt;/strong&gt; Ce qui décide si le point précédent recevra encore un correctif de sécurité un jour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Le contrat de redémarrage.&lt;/strong&gt; Quels conteneurs reviennent seuls après un boot, et lesquels, délibérément, non — l’instance en sommeil, par exemple, doit rester à terre, sinon deux versions de l’application se disputeraient la même base.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Rien d’autre ne compte vraiment. Cette liste est assez courte pour être vérifiée à la main, avant et après — ce qui rend impardonnable de ne pas la vérifier.&lt;/p&gt;
&lt;h2&gt;Le seul dégât durable est tombé sur le point trois&lt;/h2&gt;
&lt;p&gt;Une montée de version désactive ou supprime les sources apt tierces. Ce n’est pas un défaut : ces sources sont compilées pour la version qu’on quitte, et les garder actives pendant le saut est la meilleure façon de casser le système. L’outil a raison de le faire, et il le dit.&lt;/p&gt;
&lt;p&gt;Ce qu’il ne fait pas, c’est vous le rappeler ensuite. Notre source Docker avait disparu. Rien n’était cassé : le démon tournait, les conteneurs avec, le site servait. Mais le paquet installé était compilé pour la distribution précédente, et plus aucun dépôt n’était en mesure de le remplacer. &lt;strong&gt;Le mode de défaillance n’est pas un service qui s’arrête ; c’est un gestionnaire de paquets qui devient à la fois autoritaire et vide.&lt;/strong&gt; Il annonce que tout est à jour, et il dit vrai à propos d’un univers qu’il ne voit plus.&lt;/p&gt;
&lt;p&gt;Rétablir la source tient en quatre lignes, sans redémarrage. Ça a révélé sur-le-champ sept versions mineures de dérive accumulées en silence. Rien n’aurait jamais levé la main : ni le démon, qui fonctionne ; ni la supervision que nous n’avons pas ; ni &lt;code&gt;apt&lt;/code&gt;, qui n’avait plus rien à quoi se comparer.&lt;/p&gt;
&lt;h2&gt;L’angle mort : les fichiers compose&lt;/h2&gt;
&lt;p&gt;Vient ensuite le constat structurel, celui qui mérite d’être emporté.&lt;/p&gt;
&lt;p&gt;Notre service de base de données n’avait pas de politique de redémarrage. Après un boot, tous les autres conteneurs seraient revenus, et pas Postgres. Le défaut est trivial : une ligne manquante. Sa durée de vie ne l’est pas : il était là depuis des mois, et il ne pouvait se manifester qu’au redémarrage de l’hôte — lequel n’était pas survenu de tout ce temps.&lt;/p&gt;
&lt;p&gt;La bonne question n’est pas comment il a été écrit. C’est pourquoi rien ne l’a attrapé. Et la réponse se généralise bien au-delà de notre installation : &lt;strong&gt;les fichiers compose sont la seule configuration de production que personne ne possède.&lt;/strong&gt; Ils ne sont pas cuits dans l’image, donc la construction ne les voit jamais. Aucun test ne les exerce, parce que les tests tournent contre l’application, pas contre la topologie de l’hôte. Et notre job de déploiement ne les copie même pas : il ouvre une session SSH et lance un script. Ils sont édités dans le dépôt, appliqués à la main, et validés par rien.&lt;/p&gt;
&lt;p&gt;Dans une chaîne par ailleurs entièrement automatisée — tests, analyse statique, construction d’image, déploiement sans coupure — c’est là qu’un défaut peut dormir indéfiniment. Pas dans le code que la chaîne relit vingt fois par jour, mais dans la poignée de lignes de YAML qu’elle n’ouvre jamais.&lt;/p&gt;
&lt;h2&gt;Ce que le noyau aurait pu casser, et ce que nous n’avons pas vérifié&lt;/h2&gt;
&lt;p&gt;Un saut majeur de noyau sous un Postgres conteneurisé mérite mieux que « ça a redémarré ». Trois questions valent d’être posées, et nous ne pouvons en trancher que deux :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Le répertoire de données.&lt;/em&gt; Il vit dans un volume nommé, sur le même système de fichiers, avec le même pilote de stockage. Rien n’a bougé — et c’est la raison pour laquelle la montée était survivable, pas la chance.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Les profils de confinement du démon.&lt;/em&gt; Les valeurs par défaut seccomp et AppArmor arrivent avec le paquet Docker, pas avec la distribution : c’est pourquoi les conteneurs ont retrouvé le même environnement de l’autre côté.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;La sémantique de durabilité.&lt;/em&gt; Savoir si un nouveau noyau change quoi que ce soit pour Postgres avec nos options de montage est une question à laquelle nous n’avons pas répondu. Ça a tenu, ce qui ne prouve rien. Nous l’écrivons plutôt que de revendiquer une vérification que nous n’avons pas faite.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;La mesure existait déjà&lt;/h2&gt;
&lt;p&gt;Reste l’obligation posée en introduction : connaître le coût réel de la coupure qu’on a choisi d’accepter. Nous avons annoncé une durée en lisant les journaux des conteneurs : l’écart entre la dernière erreur d’un processus et le démarrage du suivant. Ce nombre décrit l’hôte, pas les visiteurs. Il exclut l’arrêt qui précède et la montée en charge qui suit, et il peut se tromper d’un facteur trois.&lt;/p&gt;
&lt;p&gt;Pendant ce temps, tout notre trafic passe par un CDN qui avait déjà enregistré, requête par requête, exactement ce que les gens ont reçu sur cette fenêtre. &lt;strong&gt;La mesure qu’on croyait ne pas avoir avait été prise pour nous, par un équipement qu’on payait et qu’on n’avait jamais interrogé.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Le réflexe à corriger n’est pas « ajouter de l’instrumentation ». C’est inventorier ce qui mesure déjà avant d’ajouter quoi que ce soit : le CDN, les journaux du proxy lui-même, la console du fournisseur. Nos maladresses de procédure cette nuit-là — un nom de conteneur deviné, des variables de shell qui ne survivent pas à une reconnexion SSH, un grep assez large pour remonter 41 lignes pour 2 erreurs — appartiennent toutes à la même famille et valent exactement une phrase : un runbook résout ce dont il a besoin, il ne le fige jamais. Savoir quelle instance sert le trafic en est le cas d’école : ça change à chaque déploiement.&lt;/p&gt;
&lt;h2&gt;Pourquoi on publie tout ça — et la seule chose qu’on garde&lt;/h2&gt;
&lt;p&gt;Un REX n’a de valeur que s’il est précis, celui-ci porte donc les commandes, les modes de défaillance et les trous. Ce qui soulève une question légitime : publier tout ça, n’est-ce pas offrir une carte à un attaquant ?&lt;/p&gt;
&lt;p&gt;Notre règle tient en une phrase. &lt;strong&gt;Le problème n’est jamais de nommer une version. C’est de nommer une version sur laquelle on est encore vulnérable.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;« On était en version X, ça nous a coûté ça, c’est corrigé » est une pratique de post-mortem ordinaire. Le même texte publié avant le correctif est une faiblesse encore vraie avec la cible attachée — un REX est signé, il nomme une entreprise et un domaine. Donc : corriger d’abord, raconter ensuite, et généraliser ce qui n’apprend rien. L’&lt;em&gt;écart&lt;/em&gt; — sept versions de dérive silencieuse — c’est la leçon ; la chaîne de version exacte de notre serveur, non. Ce qu’on ne publie jamais, c’est l’autre catégorie, celle qui ne s’apprend pas mais se recopie : noms de machines, chemins, la chaîne de clés qui déchiffre une sauvegarde. La frontière n’est pas « sensible ou anodin », elle est : &lt;strong&gt;est-ce que ça enseigne, ou est-ce que ça ouvre ?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Ce que ça a donné&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quatre points de couplage&lt;/strong&gt; isolés entre une application conteneurisée et son hôte : noyau, démon, sources apt du démon, contrat de redémarrage. Assez peu nombreux pour être vérifiés à la main, avant et après.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une voie de mise à jour gelée, rétablie&lt;/strong&gt; : le démon avait dérivé de sept versions mineures sans que rien puisse le signaler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un angle mort structurel, nommé&lt;/strong&gt; : les fichiers compose, seule configuration de production qu’aucune construction, aucun test et aucun déploiement ne lisent jamais.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Une mesure récupérée plutôt que construite&lt;/strong&gt; : le CDN avait déjà enregistré ce que les visiteurs ont vu, gratuitement.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bonnes pratiques&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Écrire ses points de couplage une fois pour toutes. Sur un hôte de conteneurs il y en a quatre, ce sont toujours les mêmes, et les vérifier prend dix minutes.&lt;/li&gt;
&lt;li&gt;Après une montée de version, re-vérifier d’abord les sources tierces : un gestionnaire de paquets qui n’a plus rien à quoi se comparer annonce que tout est à jour, et il ne ment pas.&lt;/li&gt;
&lt;li&gt;Chercher ce que l’automatisation ne possède pas. Dans une chaîne entièrement automatisée, le défaut qui survit est dans le fichier que la chaîne n’ouvre jamais.&lt;/li&gt;
&lt;li&gt;Inventorier ce qui mesure déjà — CDN, proxy, console du fournisseur — avant d’ajouter de l’instrumentation. Le chiffre qui vous manque a souvent déjà été enregistré.&lt;/li&gt;
&lt;li&gt;Un runbook résout, il ne fige pas : quelle instance sert le trafic, son nom de conteneur, l’identifiant du service — tout ça se demande au moment de l’appel.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Points de vigilance&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;La panne dangereuse n’est pas le service qui s’arrête, c’est celui qui continue de fonctionner en perdant sa capacité à être mis à jour. Il n’émet rien.&lt;/li&gt;
&lt;li&gt;« Ça a redémarré » ne prouve rien sur la durabilité. Écrire les questions auxquelles on n’a pas répondu plutôt que revendiquer une vérification qu’on n’a pas faite.&lt;/li&gt;
&lt;li&gt;Un défaut qui ne peut se manifester qu’au redémarrage de l’hôte a la durée de vie de l’intervalle entre deux redémarrages. Sur un serveur qui se tient bien, c’est des mois.&lt;/li&gt;
&lt;li&gt;Un journal interne dit quand un processus est mort, jamais quand les visiteurs ont cessé d’être servis. Seule une mesure prise devant la pile le dit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Corriger avant de raconter : un REX qui décrit une faiblesse encore ouverte, signé de son nom, n’est pas de la transparence — c’est une notice d’utilisation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
        <entry>
        <title>Construire un SaaS complet, prêt pour la production, avec un agent de code</title>
        <link href="https://mesa.black/saas-complet-avec-un-agent-de-code/"/>
        <id>https://mesa.black/saas-complet-avec-un-agent-de-code/</id>
        <updated>2026-09-26T00:00:00+00:00</updated>
        <summary>Déploiement sans interruption, paiements réels, authentification sans mot de passe — en binôme avec une IA.</summary>        <content type="html">&lt;p&gt;Comment on a conçu, sécurisé et déployé une plateforme entière — bascule blue-green sans
coupure, prélèvements bancaires réels, connexion par passkey, site trilingue, sauvegardes
chiffrées — en travaillant en binôme avec un agent de code.&lt;/p&gt;
&lt;p&gt;Le REX détaille l’architecture retenue, ce que l’agent a réellement accéléré, et surtout
les endroits où il a fallu reprendre la main.&lt;/p&gt;
</content>
    </entry>
        <entry>
        <title>Garantir des retours d&#039;expérience légitimes sans équipe de modération</title>
        <link href="https://mesa.black/legitimite-sans-moderation/"/>
        <id>https://mesa.black/legitimite-sans-moderation/</id>
        <updated>2026-09-25T00:00:00+00:00</updated>
        <summary>Miser sur l&#039;identité vérifiée plutôt que sur la censure — et les deux bugs qui nous ont fait douter.</summary>        <content type="html">&lt;p&gt;Une plateforme de retours d’expérience ne vaut que par la confiance qu’on accorde à ce
qui y est publié. Un seul faux témoignage, et c’est la crédibilité de tout le corpus qui
vacille. La question qu’on s’est posée : comment garantir la légitimité de chaque
publication, à l’échelle, sans monter une équipe de modération — ni confier le tri à une
IA, qui juge la forme d’un texte et non la légitimité de celui qui le publie ?&lt;/p&gt;
&lt;p&gt;Notre réponse remplace la modération du contenu par la &lt;strong&gt;vérification de l’émetteur&lt;/strong&gt; :
pas d’entreprise, pas de publication ; une substance minimale exigée ; un numéro de TVA
intracommunautaire validé auprès du registre européen ; une traçabilité complète.&lt;/p&gt;
&lt;p&gt;Le REX raconte aussi les &lt;strong&gt;deux pièges&lt;/strong&gt; rencontrés en chemin, tous deux dans la même
famille : un service tiers qui répond &lt;code&gt;200 OK&lt;/code&gt; sans avoir pu vérifier quoi que ce soit,
puis un état « inconnu » qui recouvrait en réalité deux situations opposées — « je n’ai
pas pu vérifier maintenant » et « je ne pourrai jamais vérifier cette valeur ». Le second
a laissé passer n’importe quelle chaîne de caractères pendant des mois.&lt;/p&gt;
</content>
    </entry>
    </feed>
