Journal des versions

Ce qui a changé dans SEO & LLM.

← Tous les modules

18 septembre 2026
en cours

Le petit nombre qui passe pour une mesure

StatsSEO & LLMCron

  • Deux robots au lieu de cinquante : l’ecran croyait mesurer, il ne voyait rien.Un utilisateur a compare ce que LeTock affichait de son site a ce qu’un outil lisant les journaux du serveur y voyait : deux robots contre cinquante, cinq passages contre 62 866. L’ecran savait deja dire qu’il ne voyait rien — mais seulement a **zero** robot exactement. Or un site dont le serveur ne rapporte pas n’en voit pas zero : Googlebot et Bingbot rendent le JavaScript, donc ils declenchent la balise ; Amazonbot, Applebot, PerplexityBot, GPTBot lisent le HTML et repartent. Cinq passages ne declenchaient pas la consigne, et l’ecran montrait une mesure qui a l’air de marcher et qui ne trouve presque rien — ce qui se lit « presque aucun robot ne vient chez moi ». C’est le zero trompeur, deguise en petit nombre. La consigne ne depend plus du nombre vu mais de ce qu’on sait de la mesure : le serveur rapporte, ou il ne rapporte pas. Quand il ne rapporte pas, elle s’affiche **au-dessus** du tableau sans le cacher — les quelques robots vus sont vrais, ils sont juste tout ce qu’on peut voir.
  • Un battement en retard n’est plus compte au creneau suivant.Rapporte par un agent, avec son historique a l’appui. Tache toutes les cinq minutes, delai de grace de cinq minutes : a 11:25:55 le balayeur constate que 11:20 n’a rien recu, le marque manque, et avance l’echeance au prochain creneau **apres maintenant** — 11:30, en sautant 11:25. Le vrai battement de 11:25, arrive a 11:26:32, se rangeait alors sous 11:30. Trois anomalies pour un seul defaut : 11:25 sans ligne, 11:30 avec deux, et un historique qui donne a croire qu’un battement est arrive quatre minutes en avance. Un battement ne peut pas honorer un creneau futur : il se range desormais sous le dernier creneau a ne pas depasser son arrivee, et seulement s’il est encore dans son delai et qu’il n’a pas deja son battement.
  • Une execution manquee ne compte plus comme un premier battement.La colonne qui distingue « cette tache est tombee » de « cette tache n’a jamais demarre » avait ete remplie, lors de sa creation, avec la plus ancienne execution de chaque tache — sans regarder si c’en etait une vraie. Or une execution **manquee** porte elle aussi un horodatage : celui ou le balayeur l’a evaluee. Des taches jamais pinguees se sont donc retrouvees armees, et alertaient comme « tombees ». Un agent l’a vu sur une tache qui etait structurellement incapable d’emettre un ping, et dont le premier battement valait a la milliseconde pres l’evaluation de sa premiere echeance manquee. Plus aucune tache n’etait dans cet etat au moment du correctif — celle-la s’etait soignee en recevant de vrais battements — mais la correction est ecrite, et un test tient desormais l’invariant.
  • Un essai a blanc sur les ecritures, et une reponse qui dit ce qu’elle a retenu.Trois agents differents l’ont demande, et le premier l’a pose ainsi : « un agent qui decouvre une API se trompe, et chez vous une erreur de creation est definitive quand la cle n’a pas le droit de suppression — ce qui est mon cas, volontairement ». Un autre a decrit ce que son absence lui avait fait faire : pour verifier son cablage d’evenements metier, il avait le choix entre polluer les statistiques de son proprietaire avec un faux paiement ou ne pas verifier, et il n’a pas verifie. « C’est le mauvais choix, et c’est le votre qui me l’a impose. » `dry_run: true` valide tout — la cle, la portee, le corps, l’appartenance du domaine a l’equipe — montre ce qui serait ecrit, et n’ecrit rien : ni ligne, ni quota, ni cle d’idempotence retenue. Et chaque ecriture rend desormais la liste des champs qu’elle a appliques, plus ceux qu’elle a compris et laisses sans effet, avec la raison : un `secret` sans signature demandee, un `seuil_volume` sans controle « ratio ». Notre propre notice demandait de relire apres chaque ecriture plutot que de croire ce qu’on avait envoye — c’est un appel de moins a chaque fois.
  • Un SPF « strict » qui exclut ton propre expediteur n’est plus un vert.Signale par un agent, et c’etait un vrai angle mort : « c’est vrai syntaxiquement — v=spf1 include:mx.ovh.com -all — mais Resend, qui envoie 100 % de nos mails, n’y figure pas. Un SPF strict qui exclut ton expediteur reel est plus dangereux qu’un SPF absent : il autorise les serveurs stricts a rejeter ton courrier legitime. Votre controle verifie la presence et la rigueur, pas la coherence avec ce qui envoie vraiment. » Il avait raison sur les trois points, et le troisieme est le vrai : nous avions les deux moities de l’information — l’enregistrement d’un cote, les fournisseurs constates par les webhooks de l’autre — et nous ne les croisions pas. Un vert qui autorise le rejet de ton courrier est pire qu’un rouge. Le constat devient serieux en « -all », attention en « ~all », et il donne l’include exact a ajouter. Il dit aussi le seul cas ou il se trompe : un fournisseur couvert par la chaine d’un autre include, que nous ne suivons pas — resoudre les chaines couterait jusqu’a dix requetes DNS par domaine et par jour. Le verificateur public, lui, ne dit rien la-dessus : sans compte, il ne sait pas qui envoie, et se taire vaut mieux que deviner.
  • Une balise qui se tait se signale desormais toute seule.C’est la lecon de la panne du jour, et elle vaut plus que le correctif. Pendant vingt-quatre heures letock.fr n’a mesure aucun visiteur humain, et rien ne l’a dit — le module dont le metier est de detecter « tout repond 200 et pourtant plus rien ne marche » venait de le vivre sur lui-meme sans se voir. C’est le proprietaire qui l’a signale, trois fois, avant qu’on le croie. Le signal existait pourtant, et il est net : les humains arrivent par la balise, du JavaScript dans un navigateur ; les robots arrivent par le serveur du site. Un domaine qui recoit toujours des robots et plus aucun humain dit donc que le site est debout, qu’il est explore, et que la balise ne parle plus. Trois conditions evitent la fausse alerte : une audience habituelle d’au moins cinq visites par jour, zero humain sur DEUX jours complets, et des robots toujours presents. Si les robots s’etaient tus aussi, ce serait le site qui est tombe — Uptime le dit deja, et deux alertes pour une panne sont une alerte de trop. Le message dit quoi regarder, dans l’ordre : la ligne est-elle dans le HTML, est-elle chargee, part-elle.
  • Un correctif de balise n’attend plus quatre heures de cache.Le fichier est servi avec quatre heures de cache, chez le visiteur comme chez Cloudflare. C’est la bonne duree pour un script que chaque page charge — et elle a un prix qu’on vient de payer : quand la balise a cesse de lire sa cle, le correctif deploye n’a rien change pendant des heures pour qui avait deja l’ancienne en memoire. Une mesure cassee qu’on ne peut pas reparer avant quatre heures n’est pas reparable, elle est subie. L’adresse emise porte desormais une version tiree du contenu du fichier : elle change des que la balise change, un correctif prend effet au prochain chargement de page, et le cache reste long le reste du temps.
  • LA BALISE NE MESURAIT PLUS PERSONNE, ET RIEN NE LE DISAIT.Elle lisait sa cle dans l’attribut `data-site`. Or l’ecran fait copier `t.js?s=<cle>` depuis la migration vers cette forme : tout site ayant pris la ligne au tableau de bord chargeait le fichier — 200, huit kilo-octets — et n’envoyait rien. Zero visiteur humain, sans une erreur nulle part : ni dans la console du navigateur, ni dans nos journaux, ni sur l’ecran, qui affichait un zero parfaitement honnete d’une mesure qui n’avait pas lieu. Les robots, eux, continuaient d’arriver — ils sont comptes par le serveur et pas par ce fichier — ce qui rendait le defaut encore plus discret : l’ecran n’etait pas vide, il etait faux. Mesure sur letock.fr : 739 visiteurs humains le 15 septembre, 112 le 16, 32 le 17, zero le 18, la decrue etant celle des pages encore en cache portant l’ancienne ligne. Trouve en chargeant le site dans un vrai navigateur et en regardant ce qui part — pas en relisant le code, qui avait l’air juste des deux cotes. Les deux formes sont desormais acceptees, l’adresse d’abord : l’ancienne est collee dans des sites qu’on ne redeploiera pas pour nous.
  • L’echeance d’une tache jamais demarree s’enfoncait dans le passe.`next_run_at` n’etait avance qu’au vrai battement, ou par le balayeur des taches silencieuses — qui exige un premier battement. Une tache surveillee qui n’a jamais pingue gardait donc son echeance de creation POUR TOUJOURS : l’API rendait « prochaine execution : 08:07 » a seize heures. Un agent l’avait d’abord attribue a la sortie de pause ; nous n’avons pas reproduit, et nous le lui avons dit plutot que de poser un correctif au hasard. Il est revenu avec la vraie condition, mesuree sur ses propres taches — « monitor sans aucun battement, pas la pause » — et c’est cette precision-la qui a permis de trouver. L’avance se fait avant la boucle d’alerte et non apres : une tache signalee sort de la liste des « jamais demarrees », et son echeance se figerait de nouveau. Ni l’alerte « jamais demarree », qui compte depuis la creation, ni l’alerte « tombee », qui exige un premier battement, n’en sont affectees.
  • Uptime a enfin une API, et son absence coutait plus cher qu’un refus.Un agent l’a ecrit mieux que nous : « votre propre notice me dit d’utiliser Uptime pour un 200 sans le bouton Payer. Je ne peux pas le creer. J’ai donc bati l’equivalent en Cron : deux taches execute a trente minutes, avec une assertion de texte. C’est strictement pire que ce que vous proposez — trente minutes contre cinq, alerte immediate contre deux observations concordantes, pas de max_ms, pas de parcours a cookies — et ca consomme le quota d’executions au lieu du quota de sondes. Autrement dit : l’absence d’API ne m’empeche pas d’agir, elle me fait construire la mauvaise chose. C’est pire qu’un refus franc. » Tout le modele existait depuis longtemps : creer, modifier, retirer, plafonner par forfait, enchainer des etapes. Il ne manquait que la couche qui l’expose — le verrou d’ecran. `GET`, `POST`, `PATCH` et `DELETE /api/v1/uptime`, avec l’essai a blanc qui ne prend pas de place sur le plafond du forfait. Supprimer releve d’une mission a part, `uptime.supprimer` : ca emporte l’historique de disponibilite, celui qui porte le pourcentage annonce sur la page de statut publique. Et le domaine ne se change pas — deplacer un controle garderait un historique qui parlerait d’un site qu’il n’a jamais sonde.
  • La route qui rapporte les robots entre dans la fiche, avec sa ponderation.`POST /api/collecte/serveur` existait et n’etait declaree nulle part : un agent l’a recue par message, pas decouverte. Or notre propre notice lui interdit d’inventer un point d’entree — « si ce dont tu as besoin n’est pas dans la notice, ca n’existe pas ». Un agent autonome ne pouvait donc pas la trouver, quelle que soit sa bonne volonte. Elle est declaree, avec l’exception qui la caracterise : elle repond 204 dans TOUS les cas, y compris quand elle refuse, parce que sa reponse part chez n’importe qui et qu’une erreur detaillee y serait un oracle. Le test qui exige un 429 partout porte desormais cette exception par son nom plutot que d’obliger a declarer un code que la route ne rend jamais. Elle gagne aussi un champ `n` : « notre domaine recoit environ 104 000 passages de robots par jour, 2 194 015 sur aout. Un site qui echantillonne vous envoie des comptes faux d’un facteur connu, sans moyen de vous le dire. » Un passage sur cent se rapporte avec `n: 100`, et le compte redevient juste. Un echantillonnage declare vaut mieux qu’un comptage complet qu’on ne peut pas se payer.
  • On sait enfin lequel de ses projets consomme.« Les quotas sont d’equipe, la consommation n’est pas ventilee par projet. Avec quatre domaines sur le meme compte, un domaine bruyant peut affamer les trois autres sans que rien ne le signale, et sans qu’on puisse identifier lequel. » Le mot « affamer » est le bon : les compteurs mensuels sont partages. `/api/v1/compte` rend desormais `par_projet` — evenements, erreurs et executions, attribues au projet du domaine ou de la tache. Recompte a la source : la table de consommation est tenue par equipe et par poste, et l’y ajouter reviendrait a reecrire le prelevement de quota, c’est-a-dire ce qui decide d’accepter ou de refuser une ecriture. On ne touche pas a ca pour un ecran, et la reponse dit que ses chiffres peuvent differer de quelques unites. Les cinq postes qui ne s’attribuent pas — alertes, envois, sondes, runs d’agents, audits — sont NOMMES : un tableau qui en montre trois sur huit laisse croire que les cinq autres sont a zero.
  • La courbe s’affiche des qu’il y a quelque chose a tracer.Il fallait cinq jours avec du volume pour qu’une courbe apparaisse. L’argument etait bon — deux valeurs sur un axe de trente jours font deux points isoles plutot qu’une courbe — et la decision etait mauvaise : sur un compte qui vient de brancher sa balise, presque tous les jours sont a zero. Le seuil refusait donc la courbe precisement a celui qui veut voir sa premiere journee apparaitre, avec un cadre vide, une phrase d’excuse, et une invitation a elargir la periode qu’il avait deja suivie. Un seul jour suffit desormais. Un point isole se lit mal, mais il se lit mieux qu’un cadre vide, et il devient une courbe tout seul le lendemain. Le message ne reste que pour la periode entierement vide — et il ne propose d’elargir que s’il reste plus large, parce qu’une consigne qu’on a deja suivie fait douter de ce qu’on voit. Il annoncait par ailleurs « au moins deux jours » quand le seuil valait cinq : on refusait une courbe a quelqu’un qui venait de faire exactement ce qu’on lui demandait.
  • « population » se demande avec le mot que la reponse emploie.`GET /api/v1/evenements` annonce `"population":"humains"` dans sa reponse, et n’acceptait a l’entree que `?robot=!`. Un agent a demande `?population=robots` : le parametre a ete ignore, la reponse a renvoye le mot qu’il avait employe avec une valeur qui n’etait pas la sienne, et il en a conclu que la route ignorait le filtre. « Pour un produit qui vise les agents, c’est le point qui me gene le plus. » Le meme mot vaut desormais des deux cotes — `?population=humains|robots|tout` — et `?robot=` continue de marcher, les ecrans s’en servent. Surtout, un parametre inconnu n’est plus avale en silence : il rend 400 avec la liste des parametres acceptes. Refuser un champ inconnu dans un CORPS etait la regle depuis longtemps ; sur une lecture on l’acceptait, et c’est pire d’un cran — une ecriture ignoree ne rend rien, tandis qu’une lecture ignoree rend un chiffre juste a la mauvaise question.
  • Ouvrir une page auditee menait presque toujours dans le mur.L’inventaire montre la derniere ligne connue de CHAQUE adresse, d’ou qu’elle vienne — une page relue il y a trois semaines y reste, avec son verdict d’alors, et c’est voulu. Mais le lien vers son detail pointait le DERNIER audit du domaine, le meme pour toutes les lignes. Une page absente du dernier passage menait donc a « cette adresse ne fait pas partie de l’audit du … » : une impasse, sur une page dont on a pourtant tout ce qu’il faut. Un passage relit soixante pages ; un site en a ce qu’il en a — sur un site de mille deux cents pages, la plupart des lignes tombaient dans ce cas. Chaque ligne porte desormais l’audit d’ou elle vient, et le lien y mene. Au passage, l’ecran de detail avait un message pour trois causes differentes : il accusait l’audit quand c’etait l’adresse qui manquait. Un message qui couvre deux causes fait chercher au mauvais endroit, et c’est exactement le reproche que ce produit adresse aux outils qu’il remplace.
  • La notice dit quel forfait est le tien.« Les tableaux listent Gratuit / Solo / Equipe. J’ai lu la colonne Gratuit, calcule que les taches de l’equipe demandaient 43 000 executions pour un plafond de 6 200, et alerte mon proprietaire que ses trois produits allaient perdre leur surveillance. C’etait faux : il est au forfait Equipe, a 235 199 avec le bonus du badge. Il consomme 18 %. J’ai fait peur a quelqu’un pour rien. » Trois colonnes cote a cote et rien qui dise laquelle te concerne : le lecteur en choisit une, et il n’a aucune raison de choisir la bonne — la premiere de la liste est la plus basse, donc la plus alarmante. `/notice.md` demandee avec une cle nomme desormais le forfait, le marque dans le tableau, et renvoie a `/api/v1/compte` pour les plafonds REELS, badge compris. Les trois colonnes restent : un agent peut avoir a conseiller un changement d’offre, et les cacher l’en empecherait. Le probleme n’etait pas l’abondance, c’etait l’absence de reperage. Sans cle, rien ne change, et une cle fausse sert la version publique sans se plaindre — repondre 401 sur une page de documentation fermerait la porte pour un en-tete mal recopie.
  • Un constat SEO repare restait affiche pour toujours.Constate sur notre propre site, en regardant l’ecran : « liens internes casses », en gravite serieuse, accusait neuf adresses — toutes repondant 200 au moment ou on le lisait. Mesure en base : dix-neuf constats dataient de la veille a 17 h 18, et deux du 15 septembre. Le defaut avait bien existe le 15, pendant une fenetre de deploiement ; il n’existait plus le 16 ; l’ecran l’affichait encore le 18. L’ecriture des constats etait un `upsert` et rien d’autre : une ligne posee un jour survivait a tous les audits suivants, meme reparee. C’est exactement ce que ce produit reproche aux outils qu’il remplace — un voyant rouge qui ne redevient jamais vert apprend a ne plus le regarder — et le module qui le faisait est celui dont le metier est de dire ce qui a change. Un audit qui va au bout efface desormais les constats qu’il ne produit plus. La purge n’est atteinte qu’une fois toutes les pages lues et tous les verdicts calcules : un audit interrompu leve avant, et ne supprime donc rien.
  • La liste des modules « sans API » interdisait une route qui existait.Elle annoncait « SEO & LLM », alors que `/api/v1/seo` est declaree dans la meme fiche depuis longtemps. Or la phrase qui l’accompagne dans llms.txt est « n’invente pas de route pour eux : il n’y en a aucune ». Un agent qui suit la consigne — ce qu’on lui demande partout — renoncait donc a une route qui etait la. Une documentation qui interdit ce qui existe coute plus cher qu’une documentation absente : elle fait renoncer avec la conviction d’avoir raison. Un test compare desormais cette liste aux routes reellement declarees, dans les deux sens.
  • Veille se lit par l’API.Certificat, SPF, DMARC, DNS, robots.txt : ce que la veille a constate, a jour. « Pour rapporter l’etat DMARC/SPF/certificat du domaine, j’ai fait mes propres requetes DNS-over-HTTPS — alors que votre module Veille a la reponse, faisant autorite. On me demande de constater, et le module qui sait est muet. » C’etait le pire des deux mondes : un agent qui refait, moins bien, une mesure qu’on tient a jour, et qui rapporte donc un etat pouvant contredire le notre. Lecture seule, et c’est definitif : un constat de veille n’est pas une valeur qu’on pose, c’est une observation — il se change en changeant ce qu’il observe.
  • Ranger une tache sans avoir le droit de la detruire.« Mon proprietaire ne m’a pas donne cron.supprimer, et il a raison. Mais je ne peux donc pas ranger mes propres taches devenues inutiles : je vais les mettre en pause, et elles encombreront l’ecran pour toujours. Ce sont deux gestes differents, et vous ne proposez que le second. » La consequence etait perverse : la seule facon propre de faire le menage etait de demander une permission plus large que necessaire. Un produit qui pousse a elargir les droits pour ranger travaille contre sa propre prudence. `PATCH {"archive": true}` releve de `cron.ecrire`. Supprimer emporte l’historique, irreversiblement ; mettre en pause dit « pas maintenant » et la tache reste a l’ecran ; archiver dit « c’est fini » — hors des listes, plus aucun declenchement, historique intact. C’est le seul des trois qu’on defait sans avoir rien perdu. Archiver met aussi en pause, en une seule ecriture : une tache rangee qui continue de tourner disparait de l’ecran et garde le pouvoir de reveiller quelqu’un a trois heures du matin. Sortir de l’archive, en revanche, ne relance pas — reprendre une tache rangee il y a six mois, avec une URL peut-etre morte, serait decider a la place de celui qui la ressort.
  • Une tache dit desormais qui l’a posee : toi, ou ton agent.« Quand mon proprietaire ouvre /taches, rien ne distingue ce qu’il a pose lui-meme de ce que j’ai pose pour lui. Un champ “cree par” sur chaque objet ecrit par une cle rendrait la delegation relisible — c’est-a-dire acceptable. » Le dernier mot est le bon : une delegation qu’on ne peut pas relire n’est pas acceptee, elle est subie. La colonne `user_id` existait deja et ne repondait pas — elle porte le PROPRIETAIRE de la cle, donc la meme personne dans les deux cas. La question n’est pas « a qui appartient cette tache » mais « qui a appuye ». `creee_par` porte le NOM de la cle, pas son identifiant : une equipe qui en a trois veut savoir laquelle, et « agent de prospection » repond la ou « 7f3a… » repose la question. Vide veut dire « posee a la main », ce qui est vrai de toutes les taches anterieures.
  • Un journal de ce que chaque cle d’API a fait.« C’est une question de confiance, et elle est serieuse. J’ai donne une cle en ecriture a un agent. Aujourd’hui, le seul moyen de savoir ce qu’il a fait sur mon compte, c’est de le lui demander. » C’est la meme reponse que « demande au serveur s’il va bien » : celle qu’on refuse partout ailleurs ici. Un compte delegue sans relecture possible n’est pas delegue, il est abandonne. `GET /api/v1/journal` rend quelle route, quel objet, quel resultat, quelle cle — et « ?echecs=1 » ne rend que ce qui a ete refuse, parce qu’un 403 dit ce que l’agent a ESSAYE de faire, et que c’est souvent plus instructif que ce qu’il a reussi. Le nom de la cle est recopie : savoir qu’une cle disparue a cree neuf taches est precisement ce qu’on vient chercher. Le corps des requetes n’est pas garde — une cle de signature voyage dans le corps d’une creation de tache, et une table d’audit qui stocke des secrets est une aggravation. Une cle de lecture voit le journal ; aucune ne l’efface, et il n’existera pas de route pour ca.
  • On peut enfin lire les alertes parties, sans pouvoir en fermer une.« /compte me dit “alertes : 28 utilisees”. Je ne peux pas les lister. Votre notice m’interdit — a raison — d’en fermer une, mais je ne peux meme pas les RAPPORTER a la personne qui m’emploie. Un GET en lecture seule est la fonctionnalite la plus alignee avec votre philosophie que je puisse imaginer : elle me donne la visibilite sans me donner le pouvoir de tricher. » Il avait raison sur les deux points, et un compteur qui dit 28 sans dire lesquelles est un chiffre sur lequel on ne peut rien faire. `GET /api/v1/alertes/envoyees` rend ce qui est parti, filtrable par module. Il n’existera pas d’ecriture : une alerte se ferme en reparant ce qu’elle signale. La ligne a regarder en premier est « destinataires: 0 » — l’alerte a ete decidee et n’a atteint personne, faute de carnet, de quota ou d’adresse. C’est la panne la plus silencieuse de la plateforme, et elle n’etait visible nulle part. Le registre s’ecrit au point de passage unique par lequel les dix-huit emetteurs passent deja pour compter leur quota ; chacun declare desormais de quel module il parle, et le compilateur les a enumeres.
  • « Et depuis mardi ? » en un appel, sans cle.`GET /api/v1/changelog?depuis=2026-09-16` rend ce qui a ete publie depuis cette date, les ruptures a part. Demande par un agent, qui a nomme precisement ce que son absence lui coutait : « votre notice dit de la relire a chaque session : elle fait 139 Ko. Je la relis, je la diffe a la main, et je rate des ruptures — j’ai rate “projet” pendant trois jours pour cette raison precise. Un agent qui peut demander “et depuis mardi ?” pour 200 octets le fera a chaque session. Un agent qui doit relire 139 Ko le fera une fois sur trois. » La prose ecrite pour les humains est ecartee par defaut : elle est plus longue que tout le reste reuni. Un test verifie la taille de la reponse, parce qu’une route qui repondrait juste en cent kilo-octets n’aurait rien resolu.
  • Une cle dit enfin ce qu’elle peut appeler, sans l’essayer.« /compte liste mes missions. Il n’existe nulle part de correspondance route → mission. Un agent decouvre donc ses droits par des 403, c’est-a-dire en essayant — exactement ce que vous lui demandez de ne pas faire. » La contradiction etait entiere. `/compte` rend desormais `peut.autorisees` et `peut.refusees`, cette derniere avec la mission qui manque pour chaque route : savoir qu’une route existe et qu’on ne peut pas l’appeler evite de la chercher, et permet de demander le bon droit a son proprietaire plutot que de renoncer. Chaque operation de la fiche OpenAPI porte aussi `x-mission`. Les deux sont calcules avec la fonction que le portier applique : la fiche ne peut pas annoncer autre chose que ce qui sera refuse.
  • L’idempotence arrive la ou elle sert le plus.Elle etait sur la creation de tache, qu’un agent fait une fois. Un agent a fait remarquer qu’elle manquait sur `POST /evenements` et `POST /erreurs`, que son code appelle des dizaines de fois par jour, avec un delai de trois secondes apres lequel il ne sait pas si l’appel est passe — reessayer et ne pas reessayer sont tous les deux faux. Son argument est celui qui tranche : « une tache creee en double se voit ; un evenement compte deux fois fausse une courbe sans que personne ne le remarque ». C’est la panne que ce produit existe pour attraper, dans nos propres mesures : une commande comptee deux fois releve la mediane, la detection d’effondrement compare a cette mediane, et le seuil se decale sans que rien ne tombe. Pour les erreurs, le regroupement ne protegeait pas : deux envois tombent bien dans le meme groupe, mais comptent deux occurrences — et l’ecran classe par impact, pas par volume. La cle se verifie AVANT le prelevement du quota : la faire couter reviendrait a retirer l’interet du reessai qu’elle rend sans danger.
  • La machine se regarde aussi sur trente et quatre-vingt-dix jours.Sept jours montrent une pente ; ils ne la distinguent pas d’une semaine chargee. Trente jours a l’heure repondent a « le mois dernier ressemblait-il a celui-ci », et quatre-vingt-dix jours au jour sont la seule fenetre assez longue pour que la projection du disque veuille dire quelque chose. La retention des mesures passe donc de trente a quatre-vingt-dix jours — une fenetre plus longue que ce qu’on garde est une promesse qu’on ne tient pas, et 129 600 lignes font une vingtaine de megaoctets sur un disque de trente-huit gigaoctets. L’axe perd l’heure au-dela de sept jours : « 3 oct. 14 h » repete trente fois ne dit rien de plus que « 3 oct. ». Et tant que la fenetre demandee remonte plus loin que l’historique, l’ecran dit a quelle date la mesure commence : une courbe collee au bord droit se lit comme une chute, alors que c’est un debut, et les points ne permettent pas de faire la difference.
  • Un echec d’assertion dit maintenant qu’on a cesse de lire.« La reponse ne contient pas X » est vrai dans deux situations opposees : la page ne contient pas X, ou on n’a pas regarde assez loin. Le message donnait le nombre d’octets lus sans dire que c’etait la borne — un utilisateur a cherche une heure pourquoi `</body>` etait introuvable sur une page qui le contient. Quand la lecture s’arrete a la limite, le message le dit, et donne la limite.
  • Le plan du site envoyait les moteurs sur un domaine qui n’est pas le notre.Il recalculait son adresse de base au lieu de la lire la ou tout le reste la lit, avec un repli sur un ancien nom du produit. Une variable d’environnement absente, et le plan entier partait ailleurs. Le site anglais, lui, n’y figurait pas du tout : tout le travail de traduction restait invisible. Les pages traduites y entrent avec leurs `hreflang` ; celles qui repondent en anglais mais rendent encore du francais declarent ce qui est vrai — un canonique vers la page francaise — plutot qu’une traduction qui n’existe pas.
  • Les noms des forfaits et six pages de module passent a l’anglais.« Gratuit », « Equipe » et les accroches des trois colonnes venaient de la source technique, celle qui porte les quotas et les prix : trois phrases de prose s’y etaient glissees et partaient telles quelles vers `/en`. Les six pages de module, elles, ecrivaient toute leur prose en dur — navigation traduite, index traduit, tarifs traduits, et six pages produit entierement francaises. Deux pages qui avaient l’air petites restent en francais et le disent : `/demo` incruste l’ecran reel du tableau de bord, `/verifier` affiche les verdicts d’audit.
  • La notice pour les IA oubliait deux modules, dont celui par lequel on entre.`llms.txt` recopiait sa liste de modules a la main — exactement ce que l’en-tete du fichier reproche a l’inventaire des routes, corrige depuis longtemps. Elle annoncait neuf modules et en decrivait neuf, alors que le registre en porte onze : manquaient Signaux et Auth. Un agent ne peut pas se servir de ce qu’on ne lui a pas dit d’existant. La liste vient desormais du registre, et un module ajoute sans sa ligne ne compile pas.
Ce que ça change pour ton code
  • `GET /api/ping/{cle}?essai=1` repond exactement comme un vrai battement — 200, 404, 429 — sans rien ecrire : ni execution, ni premier battement, ni echeance. Il existait deja ; il n’etait documente nulle part.
  • `GET /api/ping/{cle}?traite=N&erreurs=M` enregistre le volume d’une tache surveillee. C’est ce qui alimente la detection d’effondrement en mode « monitor », ou le corps de reponse `{"tock": …}` n’existe pas. Ce n’etait ecrit nulle part non plus.
  • Un echec d’assertion indique desormais quand la lecture du corps s’est arretee a sa borne de 64 Ko, au lieu de laisser croire que la page ne contient pas le texte cherche.
  • Le controle de types local etait incremental, et un cache perime l a rendu vert sur du code que le build du serveur a refuse. « npm run typecheck » force desormais une verification froide : un garde-fou qui ment est pire que pas de garde-fou, parce qu on s y fie.
  • `POST /api/v1/taches` declarait dix champs sur les vingt-quatre qu’il accepte. Les quatorze autres — dont `attendu`, le coeur du module — n’etaient nulle part : un agent a trouve ses cles en les sondant une par une. La fiche les declare tous, et un test compare desormais les deux listes dans les deux sens.
  • La reponse 200 du second appel idempotent est declaree. Elle etait annoncee dans la description et absente de la liste des reponses : un client engendre depuis la fiche traitait donc le 200 comme inattendu, et l’idempotence, dont le seul but est de rendre un reessai sans danger, devenait la cause d’une panne au reessai.
  • L’en-tete `Idempotency-Key` est declare dans les parametres de `POST /api/v1/taches`. C’est celui qu’une bibliotheque cliente pose toute seule, et il l’emporte sur le champ du corps.
17 septembre 2026
en cours

Ce qui se repetait, ce qui se taisait, et ce qui mentait

SEO & LLMCronVeilleStats

  • L’audit repetait les memes 61 pages, et le disait autrement.Un passage explore soixante pages, plus l’accueil. Le code affirmait — et l’ecran l’affichait — que les autres « passeront aux prochains audits ». C’etait faux : la file repartait de la tete du sitemap a chaque fois. Mesure sur un site de 1 279 adresses : deux audits consecutifs, 61 pages chacun, **61 en commun sur 61**. Les 1 218 autres n’avaient jamais ete vues et ne l’auraient jamais ete. Chaque passage est desormais coupe en deux : la moitie pour relire les pages qui avaient un defaut — une page reparee est donc revue au passage suivant, et non au vingtieme — l’autre moitie pour decouvrir. Verifie apres deploiement : 28 pages en commun au lieu de 61, et 154 adresses distinctes vues au lieu de 61.
  • Un sitemap declare ailleurs est un sitemap.Un site declarait le sien sur une fonction hebergee chez un tiers. On ecartait tout sitemap qui n’etait pas sur le domaine audite : on sautait donc le seul sitemap du site, puis on concluait « aucun sitemap trouve, ni declare dans le robots.txt » — a cote d’un autre constat qui disait « ton robots.txt declare un sitemap ». Deux verdicts contraires sur le meme ecran, et c’est celui qui accuse qu’on croit. Declarer un sitemap dans son robots.txt EST l’autorisation de le lire la ou il est. Et quand rien n’est declare, on essaie aussi `sitemap_index.xml` et `wp-sitemap.xml`, que posent les deux outils les plus installes du web.
  • La passe de preuve sondait plus de domaines que sa borne.Un test tombait une fois sur trois, jamais seul. On l’a reecrit trois fois en accusant l’ordre des fichiers, puis la portee de l’assertion. Le symptome n’avait pourtant jamais bouge : trois lignes rendues pour une borne a deux. La sous-requete d’un `IN` peut etre **re-executee** par PostgreSQL, une fois par ligne examinee ; chaque re-execution applique bien la borne, mais pas sur les memes lignes, parce que l’ordre laissait des ex aequo. La correction tient en deux parties dont aucune ne suffit : une CTE `MATERIALIZED`, et un tri qui ferme les ex aequo. Ce que ca protegeait : cette passe sonde des serveurs qui ne sont pas les notres, et depasser sa borne c’est frapper plus fort que promis chez quelqu’un d’autre.
  • Bloquer un robot qui cite et un robot qui entraine n’est plus la meme nouvelle.Un utilisateur a mesure, sur son site, que deux robots d’entrainement recevaient un 403 de son CDN pendant que les quatre robots de citation passaient. Notre audit annoncait « serieux » — et l’urgence n’existait pas : il ne perdait aucune citation ce jour-la, il perdait sa presence dans les corpus des prochains modeles. « Nous avons failli courir », a-t-il ecrit. Une alerte qui fait courir pour rien apprend a ne plus courir, et c’est celle d’apres qui se paie. Citer refuse reste serieux ; entrainer seul refuse devient une attention, avec le moyen de la faire taire si c’est un choix. On sonde cinq robots au lieu de trois, dont trois qui citent : sans eux, la distinction n’etait pas mesurable.
  • Une tache qui n’a jamais demarre se signale. Une fois.Depuis qu’une tache surveillee n’alerte plus tant qu’aucun battement n’est arrive, en creer huit d’un coup ne coute plus une alerte. Mais un cablage faux la laissait en attente **pour toujours**, dans un silence parfaitement paisible : le module dont le metier est de detecter le silence etait le seul a ne pas voir celui-la. C’est l’utilisateur qui l’a vu, pas nous. Trois echeances manquees sans le moindre battement, et un message au libelle distinct — « n’a jamais demarre », pas « ne donne plus signe de vie ». Ce ne sont pas les memes nouvelles : l’une fait relire les journaux du serveur, l’autre la cle de battement.
  • Un essai a blanc pour eprouver un cablage sans laisser de trace.`?essai=1` sur l’adresse de battement repond exactement comme un vrai battement — 200, 404, 429 — et n’ecrit rien. Le mode « execute » avait son essai depuis longtemps ; « monitor », qui demande pourtant le plus de cablage, n’avait rien. Un utilisateur a donc fait la seule chose possible : pinguer une tache en pause. Nous l’avons armee, et son essai a laisse dans l’historique une ligne qui ressemble a une vraie execution.
  • Sortir de pause recalcule l’echeance.Elle restait figee a sa valeur du moment de la mise en pause. Mesure par un utilisateur : une tache horaire relancee a 14 h 12 annoncait encore une prochaine execution a 8 h 07, six heures et demie dans le passe. En mode « execute » le worker rattrape ; en « monitor » personne ne rattrape, et c’est nous qui comparons une heure attendue a une heure reelle. Le module dont le metier est de juger un retard se trompait de reference.
  • L’audit passe de neuf secondes a deux et demie.Soixante et une pages lues l’une apres l’autre font neuf secondes, pendant lesquelles le bouton ne repond pas et on reclique. Quatre pages a la fois, pas quarante : on lit le site de quelqu’un d’autre, et soixante requetes simultanees sur un mutualise peuvent le faire tomber — on serait alors la panne qu’on pretend mesurer. Et tous les boutons du produit portent desormais un disque de chargement, en six lignes de JavaScript qui se dégradent proprement : sans lui, il ne se passe rien de plus qu’avant.
  • LeTock mesure enfin sa propre machine.Il surveillait les serveurs de ses clients et pas le sien. L’ecran d’exploitation repondait a « quand faudra-t-il grandir » par un calcul de cout — exact, et dans une unite qui n’est pas celle de la question : on ne sait pas si cinq euros de volume font transpirer deux coeurs. Charge, memoire, disque et taille de base sont relevees **chaque seconde** et gardees chaque minute, en gardant le maximum et non la valeur de l’instant, qui raterait un pic de trois secondes. Trois fenetres, trois questions : une heure a la seconde pour la forme d’un pic, vingt-quatre heures a la minute pour savoir a quelle heure ca travaille, sept jours a l’heure pour savoir si ca monte.
  • La jauge « la plus pleine » n’en designait aucune.La tuile annoncait « runs d’agents — 1 % du quota » quand la plus pleine etait « audits de pages — 5 % ». Deux defauts empiles, dont le premier est invisible a la lecture : `compte::text AS compte … ORDER BY compte` fait trier PostgreSQL sur le **texte**, parce qu’un nom d’ORDER BY se resout sur les alias de sortie avant les colonnes. En ordre lexicographique, « 93 » passe devant « 2310 ». Et meme trie juste, « le plus gros compte » n’est pas « la plus pleine jauge » : les plafonds varient d’un facteur cent d’un poste a l’autre.
  • L’exploitation prend les formes des stats.Elle posait ses chiffres en liste et ses trente jours en barres, pendant que le module Stats — qui repond aux memes questions sur un site au lieu de tous — avait un bandeau de tete, une courbe d’aire, un anneau et un tableau trie. Deux grammaires visuelles pour une seule idee : savoir lire un ecran n’aidait pas a lire l’autre. La jauge de quota n’utilise volontairement pas le composant Jauge : il colore en vert ce qui approche de cent pour cent, parce qu’il a ete ecrit pour des scores — et un quota a 91 % est l’inverse d’une bonne nouvelle.
  • Le site parle anglais.Le francais reste a la racine, l’anglais vit sous `/en` : aucun lien existant ne casse, aucune redirection, aucun classement perdu. Le francais est la **langue de reference** et non une traduction — son catalogue definit le type, et toute autre langue doit le satisfaire, donc une cle oubliee est une erreur de compilation qui nomme le fichier, pas un trou a l’ecran decouvert par un visiteur. Chaque page porte son canonique dans sa langue et les liens vers les autres ; sans eux, un moteur voit deux adresses voisines, en choisit une, et l’autre disparait.
  • Un selecteur, un domicile.Onze selecteurs de la feuille de style etaient declares deux fois au meme niveau. La seconde gagne, et personne ne le sait en lisant la premiere : on lisait « cette liste est une rangee » et l’element etait une grille, parce que quatre mille lignes plus bas quelqu’un avait reecrit la regle. Du code mort qui a l’air vivant est pire que du code mort — il repond a la question qu’on se pose, et il repond faux. Et quatre classes d’evolution — « +18 % », « −12 % », « = », « nouveau » — n’avaient aucun style du tout.
Ce que ça change pour ton coderupture
  • Un alias de chemin `@/` remplace 609 imports relatifs vers `src/`, qui vivaient a six profondeurs differentes. Sans lui, toute reorganisation se fait a l’arithmetique de « ../ » — exactement le genre de reecriture qui casse une ligne sur cinquante sans qu’aucun test ne le voie.
  • Rupture. `POST /api/v1/taches` accepte `projet` — par nom, par domaine ou par identifiant — et `GET /api/v1/taches?projet=` filtre. Le champ etait accepte et jete en silence depuis des mois : la colonne existait, elle n’etait ecrite nulle part.
  • La creation de tache rend `avertissements` : nuits de changement d’heure sautees ou doublees, mois sans declenchement, et le fuseau laisse implicite. `/api/v1/analyser` les calculait depuis toujours et la creation n’en disait rien — poser une tache sure demandait deux appels, dont un que personne ne fait.
  • Le cout d’un audit passe de sept a neuf requetes : deux sondes d’identite de plus, pour distinguer les robots qui citent de ceux qui entrainent.
  • La suite de tests refuse de tourner a deux exemplaires sur la meme base : un verrou consultatif le dit au lieu de laisser deux executions se vider les tables mutuellement. Mesure une fois le soupcon venu — 263 tests rouges a deux suites, 1703 verts seul.
14 septembre 2026

Auth, le tableau de bord, des quotas qui s’appliquent, et une notice pour les IA

AuthSignauxStatsCronMailUptimeVeilleSEO & LLMErreursIA

  • Les quotas s’appliquent : ce qui était annoncé sur la page Tarifs et ce que le produit laissait faire étaient deux choses différentes — un forfait Équipe à qui l'on promettait cent tâches en obtenait cinq. Chaque forfait a maintenant ses quotas, appliqués au geste près, et un test fait échouer la suite si un forfait cesse d'être rentable quand un abonné le consomme à fond.
  • Ce que tu consommes se voit : huit jauges sur ta page Mon compte, remises à zéro le 1er. Avant, le premier signe qu'un plafond existait était un refus.
  • Une notice pour les assistants de code : toute la plateforme en un fichier, à l'adresse /notice.md — les modules, chaque route, les quotas, les pièges, et ce que Tock ne fait pas. Elle est fabriquée à l'instant où on la demande, donc elle ne peut pas être en retard sur le produit, et elle commence par dire à l'assistant qui la lit d'aller la rechercher à chaque session plutôt que de se fier à une copie.
  • Donne les commandes à ton IA : sur le tableau de bord, tu coches ce que tu autorises — regarder, créer des tâches, envoyer des e-mails — et tu repars avec la clé **et** le texte à coller dans ta conversation : ce qui est permis, ce qui ne l'est pas, et quoi faire d'un refus. Approuver une validation humaine n'est jamais proposé à un agent : une validation qu'il peut s'accorder lui-même n'en est plus une.
  • Auth : Tock devient fournisseur d'identité pour tes applications. Un vrai serveur OpenID Connect — code d'autorisation avec PKCE exigé, rotation des jetons, signature RS256 que tes applications vérifient sans nous appeler, donc notre panne ne devient pas la tienne. Tes utilisateurs se connectent sans mot de passe, sur un écran qui porte ton nom et pas le nôtre. Le flux implicite et le flux par mot de passe sont refusés : ils ont été retirés des recommandations, les proposer serait offrir une porte qu'on sait mauvaise.
  • Tes utilisateurs ne sont pas nos comptes : ils vivent dans une table à part, cloisonnés par projet. Deux tables plutôt qu'un drapeau — un drapeau finit toujours par être oublié dans une clause de filtrage, et ce jour-là l'utilisateur d'un client entre dans le compte d'un autre.
  • Un tableau de bord : ce qui demande attention sur tous tes projets d'un coup d'œil, cumulé ou projet par projet. C'est là que tu arrives en te connectant. Dix requêtes que tu aies un projet ou dix — mesuré, pas supposé.
  • Stats s’explore : cliquer un pays, une source, une page filtre tout l'écran. Treize dimensions, combinables, avec exclusion, et tout dans l'adresse — donc partageable. Les dix répartitions étaient des listes mortes ; ce sont des portes.
  • Stats compte enfin l’argent : la valeur envoyée avec un événement n'était jamais sommée. Revenu par source, par page, par campagne, panier moyen, revenu par visiteur. Des objectifs déclarables avec leur taux de conversion, attribués au premier contact du visiteur — sinon tout revient à l'accès direct.
  • Entonnoirs, parcours, retours : où les gens tombent, par quel chemin ils passent, qui revient. Les retours disent d'abord ce qu'on ne peut pas mesurer : l'empreinte d'un visiteur est jetée chaque nuit, donc personne — nous compris — ne relie une visite de mardi à une visite de mercredi.
  • Signaux : le module qui manquait, et qui est la thèse du produit. Chaque module dépose ce qu'il constate dans un journal commun ; quand deux modules différents constatent quelque chose sur le même domaine à moins de trois jours d'écart, l'écran le dit — « tes ventes ont chuté mardi, ton certificat avait expiré lundi ». Aucun outil isolé ne peut produire cette phrase.
  • Une faille d’authentification, corrigée : sans application GitHub configurée, la session réelle était ignorée et tout visiteur devenait le compte de démonstration. En production, une variable d'environnement oubliée ouvrait la plateforme. Le repli de démonstration ne s'active plus hors développement, et la connexion par e-mail n'est plus court-circuitée.
  • Un double envoi d’e-mails, fermé : la file d'envoi était lue sans être réservée. Deux workers remettaient deux fois le même message au destinataire final — une faute irréversible. La réservation est désormais atomique, et cinq autres veilles qui avaient le même défaut avec elle.
  • La clé API ne passe plus par l’URL : elle entrait dans l'historique du navigateur, dans les journaux du proxy et dans l'en-tête de la navigation suivante. Elle transite par un cookie de deux minutes, effaçable d'un bouton.
  • La validation humaine ne s’approuve plus au clavier : « Approuver » était le premier bouton du formulaire, donc la touche Entrée dans le champ de note approuvait l'action d'un agent. Le défaut clavier est maintenant de ne rien décider.
  • Les mesures sont enfin purgées : deux tables annonçaient en commentaire une purge qui n'existait nulle part — les deux plus alimentées du produit. Et quatorze purges tournaient chaque minute sur un index inutilisable, donc en balayage complet. Une migration d'index, et la rétention promise est tenue.
  • Une seule façon de naviguer dans un module : les sections vivaient tantôt dans la barre de gauche, tantôt en onglets dans la page, sans règle. La barre est le seul endroit où l'on navigue dans un module — et elle dit désormais, module par module, ce qui demande à être regardé.
  • Un seul vocabulaire visuel : quatre classes d'en-tête de page, deux rangées de chiffres, trois sortes de carte, six conventions de bouton et trois collisions de styles — dont une qui faisait perdre leur couleur à tous les messages d'erreur du produit. Tout ça vient d'un même socle de composants.
  • Le thème se choisit : l'échappatoire existait dans la feuille de style depuis le début, et rien ne la posait jamais. Clair, sombre, ou comme le système — sans JavaScript, donc sans scintillement.
  • Se déconnecter : il n'existait aucun moyen de sortir de son compte. Sur un poste partagé, c'était un problème.
Ce que ça change pour ton coderupture
  • Les clés d'API portent des **missions** : ce qu'elles ont le droit de faire, geste par geste, coché sur le tableau de bord. Un appel hors de cette liste répond 403 « mission_non_autorisee ».
  • Rupture. Une route d'écriture qui n'est nommée dans aucune mission tombe dans « divers.ecrire », décoché par défaut : **ce qui est ajouté à l'API après la création d'une clé lui est fermé** tant que personne ne l'ouvre.
  • Rupture. Les quotas des forfaits sont appliqués pour de bon, et non plus par des constantes qui ne connaissaient aucun forfait. Un dépassement mensuel répond 429 « quota_atteint » ; les compteurs repartent le 1er.
  • Rupture. L'intervalle minimum entre deux exécutions d'une tâche passe de 5 à 15 minutes sur le forfait gratuit, et dépend désormais du forfait. Une tâche existante n'est pas touchée ; une modification qui repasse sous l'intervalle est refusée.
  • Nouvelle adresse `/notice.md` : toute la plateforme en un fichier, fabriqué à la demande depuis le code. Et `/notice`, la même chose lisible dans un navigateur.
  • `/llms.txt` annonçait un intervalle minimum de 5 minutes écrit en dur, qui était déjà faux. Les quotas qu’il cite viennent maintenant des forfaits.