Forme des fichiers publiés

Report au dossier de la forme arrêtée par le porteur en #8, produite par src/export.ts et poussée par lui (#12). PRD.md §4.6 nomme les fichiers ; cette page dit ce qu'il y a dedans.

C'est un contrat : la CLI (§4.7), le serveur MCP (§4.8), le veilleur (#15) et tout tiers lisent ces fichiers. Le changer casse ces quatre-là à la fois.

Les fichiers

robots.txt                     qui peut lire, et ce qu'on refuse (un signal, pas un mur)
latest.json                    l'état courant, complet
snapshots/index.json           les dates d'instantanés qui existent
catalogue.json                 le même, sans les morts ni les non testées, sans `config`
latest.md                      le tableau de bord, lisible par un humain
sante.json                     l'auto-mesure seule, quelques centaines d'octets
snapshots/2026-08-13.json      un instantané par jour, même forme que latest.json
snapshots/2026-08-13.md
llms.txt                       l'index d'entrée pour les agents
LICENSE-DONNEES.md             la licence des données, à côté des fichiers

Mesuré le 13 août 2026 sur 21 844 entrées : latest.json faisait 11,4 Mo brut, 674 Ko en gzip -9, construit en 0,3 s. C'est le chiffre compressé qui compte : rien ne transite décompressé.

Remesuré le 19 août 2026 sur 23 486 entrées, avant et après les coupes de #29 (phrase, verifie_le, version.courante) : 10 959 678 → 9 213 912 octets bruts (−15,9 %), 720 572 → 695 482 octets en gzip -9 (−3,5 %). Le brut n'est pas rien pour qui parse le fichier ; mais l'argument de la coupe n'est pas le poids, c'est qu'une constante recopiée par entrée n'est pas une mesure.

Chaque fichier porte sa licence, sans quoi elle ne suit pas la copie : objet licence en en-tête des JSON, ligne d'attribution sous le titre des .md, la même ligne en clair dans llms.txt.

latest.json

En-tête indenté (une clé par ligne), puis une entrée par ligne, triées par id. Un seul document JSON valide.

{
 "schema": "https://hultra.link/schema/entree-1.json",
 "licence": { "base": "ODbL-1.0", "contenu": "DbCL-1.0", "url": "…", "attribution": "…" },
 "passage": { "debut": "…", "fin": "…", "duree_ms": 16371 },
 "auto_mesure": { … },        ← les champs de src/mesure.ts, mot pour mot
 "sources": { "registre": {…}, "sonde": {…}, … },
 "spec_mcp": { "courante": "2026-07-28", "connues": [ … ], "source": "spec-mcp" },
 "compte": { "entrees": 23486, "par_etat": { … },
             "non_testable_par_motif": { "paquet_seul_sans_url": 10376,
                                         "jamais_sonde": 11838,
                                         "ni_url_ni_paquet": 372 } },
 "entrees": [
{"id": "…", "identite": {…}, "sante": {…}, …},
{"id": "…", …}
 ]
}

Les chiffres de cet exemple sont ceux d'un seul passage, celui du 19 août 2026 : 23 486 entrées, dont 22 586 non_testable réparties sur les trois motifs (10 376 + 11 838 + 372). Un en-tête qui mêlerait deux dates ne s'additionnerait pas, et un lecteur qui vérifie l'arithmétique de l'exemple a raison de le faire.

Une entrée par ligne n'est pas une coquetterie : elle fait du diff Git exactement la liste des entrées qui ont changé, ce dont changements(depuis) (§4.7) a besoin pour être un diff et non un recalcul. Un indent: 1 étalait la même entrée sur ~30 lignes.

Source et date : au bloc, pas au scalaire

Chaque valeur porte sa source par la famille de champs qui la contient, une famille du §2.3 = un acte de mesure. Deux clés par bloc :

  • source → clé dans la table sources de l'en-tête (déclarée une fois pour tout le fichier : nom, URL, méthode). La clé vient de la colonne entree.source, elle n'est pas déduite du protocole. Elle l'était jusqu'au 19 août 2026, et l'entrée que la boucle s'ajoute à elle-même (§4.8, #44) sortait alors avec source: "registre", le registre MCP crédité d'une ligne écrite par nous. Toute clé publiée est déclarée en en-tête, et un test le vérifie sur chaque entrée (#29) ;
  • mesure_le → date de la mesure. Absent = mesuré dans le passage courant, dont l'horodatage est dans l'en-tête passage. Ce qui vient d'une source externe plus lente (le registre) porte sa date propre.

Ce sont les noms des colonnes des tables annexes de src/base.ts : un seul mot par concept, de SQLite au JSON publié.

Ordre des clés

id, puis identite, sante, version, usage, outils, etiquettes, items, audit, config. Un bloc non mesuré est absent, jamais vide : l'absence dit « non mesuré », ce qui est déjà la règle absolue du projet. Nulls omis, sans exception.

sante.verifie_le: null en était une jusqu'au 19 août 2026 : elle disait « la question a été posée, la réponse est rien ». C'était faux. Sur les 22 586 entrées qui la portaient, 10 376 sont à paquet seul : la question ne leur a jamais été posée, et rien ne prévoit de la leur poser. L'exception justifiait donc 22 586 copies d'un null pour un cas qui n'existait pas ; etat et motif disent déjà ce qui a été fait (#29).

Les fichiers publiés, et pourquoi ils sont treize

latest.json, catalogue.json, latest.md, sante.json, llms.txt, sondes.md, les deux instantanés du jour, snapshots/index.json, robots.txt, LICENSE-DONNEES.md, format.md (ce fichier) et methode.md.

robots.txt est entré le 1er septembre 2026 : l'hôte n'en avait aucun, parce qu'un seau R2 sert des objets et n'en invente pas. Il dit Allow: / et porte la préférence en Content Signals (ai-train=no), pas en Disallow : refuser l'entraînement n'est pas refuser la visite, et cette distinction est tout le sujet sur un domaine dont les agents sont le public.

Les deux gras sont entrés le 1er septembre 2026. snapshots/index.json existe parce que snapshots/ rend 404 : l'hébergement sert des objets, il n'a pas d'index, et les deux fichiers qui annonçaient ce répertoire promettaient donc une page qui n'existe pas. Les dates y sont lues dans le journal des exports réussis, ce qui rend les trous visibles au lieu de les laisser deviner.

Les deux derniers sont entrés le 24 août 2026 pour une seule raison : trois URL publiées ne répondaient pas (#69). L'en-tête annonçait un schema: hultra.link/schema/entree-1.json qui n'a jamais existé (ni le fichier, ni l'hôte), le bloc licence renvoyait à hultra.link/LICENSE-DONNEES.md alors que le fichier est servi sur donnees.hultra.link, et le User-Agent des sondes nommait hultra.link/sondes, page jamais écrite.

Une URL de schéma qui ne répond pas est pire que pas de schéma : elle promet une vérifiabilité qui n'existe pas. Et une URL d'UA qui ne répond pas retire à la « retenue » du §4.4 la seule chose qui la rende opposable, une adresse où se faire retirer. Les trois pointent désormais là où le fichier est, dérivées de LINK_PUBLIC et non écrites en dur, pour qu'un déménagement ne les casse pas une seconde fois.

sante.portee : mesurée, et parfois absente

appel = un échange applicatif a eu lieu. http = le contenu a été parsé, sans échange applicatif. Absente = rien au-delà d'un code de retour, ou contrôle antérieur au 24 août 2026.

Elle était déduite du nom du protocole jusqu'à cette date (protocole === "shadcn" ? "http" : "appel"), donc une entrée MCP morte en DNS sortait « vérifiée à l'appel ». Mesuré sur un passage réel : 94 entrées MCP sur 235 (40 %) portaient une portée qu'elles n'avaient pas.

Son absence est un aveu, pas un oubli, et elle ne se comble pas : rétro- déduire la portée d'un ancien contrôle referait le défaut. Le parc se re-sonde intégralement en moins de 72 h, donc le trou se referme de lui-même.

Les trois durées d'auto_mesure, et pourquoi elles sont trois

Elles n'ont pas le même sujet, et les confondre est le défaut que ce format a porté jusqu'au 24 août 2026 : le champ s'appelait age_controle_plus_ancien_h, et un lecteur y comprenait « ça fait 33 h que rien ne bouge » alors que la boucle sondait toutes les 5 minutes.

ChampSujetRelevé du 24 août 2026
dernier_passage_lequand la boucle a sondéil y a 1 min
mesure_lequand ce fichier a été construitil y a 59 min (l'export est horaire)
age_entree_la_moins_fraiche_hl'entrée du parc vue il y a le plus longtemps33,8 h
retard_max_hde combien la plus en retard dépasse sa propre échéance0

age_entree_la_moins_fraiche_h n'est pas un retard. Une entrée du palier stable n'est due que toutes les 72 h (§4.4) : la voir à 33 h est le fonctionnement nominal. C'est pourquoi ce champ ne peut pas servir de critère : il flotte entre 24 et 72 h quoi qu'il arrive, et jusqu'à 168 h dès qu'une entrée passe au palier mort.

retard_max_h est la seule des quatre qui puisse dire non : zéro quand la boucle tient, croissante dès qu'elle décroche. Une entrée jamais contrôlée y compte, en retard depuis son premier_vu, c'est ce qui la fait entrer dans le nombre sans avoir à lui inventer un âge (#48).

Les fraîcheurs et la couverture portent uniquement sur les entrées sondables : une URL privée, retirée ou à gabarit n'entre ni dans joignables, ni dans controlees_24h, ni dans le retard. La compter créerait une dette que la boucle refuse volontairement de rattraper.

Renommage du 24 août 2026 : age_controle_plus_ancien_h → age_entree_la_moins_fraiche_h. Même calcul, même valeur ; seul le nom change, parce qu'il décrivait autre chose que ce qu'il mesurait. Un consommateur qui lisait l'ancien nom lit désormais undefined, c'est délibéré : un champ qui disparaît se voit, un champ qui ment ne se voit pas.

Ce qui n'y est pas, et pourquoi

  • Aucun horodatage ni latence par entrée. Sur 21 844 entrées ils produisent ~48 000 lignes de bruit par jour, dans lesquelles les vraies pannes se noient. La fraîcheur est portée par sante.depuis, l'en-tête passage, les trois durées d'auto_mesure (voir ci-dessous) et la couverture mesurée (auto_mesure.controlees_24h / joignables), la couverture MESURÉE, pas l'objectif du §4.4 : llms.txt annonçait cet objectif au présent alors que 600 adresses sur 12 530 avaient été contrôlées en 24 h, soit 4,8 % (#29). La latence et l'historique des contrôles restent en base et sortiront sur la page .md de l'entrée.
  • usage : aucune source npm / PyPI / GitHub / Smithery n'est branchée (§10.7). La forme se décidera en la branchant.
  • audit : mcp-scanner n'est pas branché (§10.5). Le bloc portait {"yara": "non_execute"} à l'identique sur chaque entrée : une constante n'est pas une mesure, et 12 074 copies d'une valeur qui ne bougera pas avant que la brique existe pèsent sans rien dire (#31). L'état se dit une fois, dans l'en-tête : sources["mcp-scanner"].etat: "non_execute". C'est ce qui garde l'absence lisible comme « non mesuré » et non comme « audité, rien trouvé ».
  • sante.phrase : « existence vérifiée, fonctionnement non testé », 22 586 fois à l'identique au 19 août 2026, pour une chaîne déductible de sante.motif. Coût mesuré le 19 août 2026 sur les octets qu'écrit src/export.ts : la clé, la valeur et la virgule font 59 octets UTF-8, × 22 586 entrées = 1 332 574 o sur les 10 959 678 o du fichier d'avant coupe, soit 12,2 %. (#29 annonce 12,5 % ; c'est le chiffre du ticket, pas celui de la mesure : la contre-vérification par la somme des trois coupes, 1 744 810 o, tombe à 956 octets de l'écart mesuré 10 959 678 → 9 213 912.) Les trois clients (src/cli.ts, src/mcp.ts, cli-py/hultra_link.py) portent la glose en dur, clée par motif et non par etat, voir le tableau des motifs ci-dessous. La changer, c'est la changer dans les trois clients ET ici, et src/differentiel.test.ts compare les trois tables à chaque exécution.
  • version.courante : c'était DEMANDE de src/sonde.ts, une constante du code, republiée 237 fois avec source: "sonde" alors qu'aucune sonde ne l'a mesurée, le sondeur crédité d'une valeur écrite à la main. Elle se dit désormais une fois, en en-tête, sous spec_mcp, avec sa vraie source (spec-mcp) et la liste connues. version.retard reste par entrée : lui se compte sur la spec négociée par cette entrée, donc il mesure (#29).
  • outils.noms : publiés depuis le 1er septembre 2026, table annexe outil (voir le bloc outils plus bas). Ce qui reste vrai : pas de clé noms vide, une entrée qui n'a jamais listé ses outils publie n seul.
  • Les descriptions d'outils : la sonde les reçoit et ne les garde pas. Les publier multiplierait le poids du fichier par ce qu'un tiers écrit dans sa prose ; les noms suffisent à décider si un serveur mérite une session.
  • Les pages .md par entrée (/e/mcp/….md) : prévues au §4.6 (« toute page doit être disponible en .md »), pas encore produites.

Les blocs

Les clés de sources au 24 août 2026 : registre (registre MCP officiel), index-shadcn (index des registres shadcn, #22), registre-a2a (a2aregistry.org, #90), liste-llmstxt (llmstxt.site, liste d'amorçage et non registre, #95), topic-marketplace (le topic GitHub claude-code-marketplace, amorçage également, #98), annuaire-apis-guru (api.apis.guru, annuaire gelé depuis le 21 avril 2023, #102), index-nanda (index.projectnanda.org, qui annonce 13 600 agents et en rend 300, #105), sonde, sonde-registre, sonde-a2a, sonde-llmstxt, sonde-marketplace, sonde-openapi et sonde-nanda (les sept sondeurs), etiquette (l'étiquetage automatique de ce dépôt, dérivé et jamais vérifié à la main, src/etiquettes.ts), auto-inscription (le serveur MCP de ce projet, inscrit dans son propre annuaire, §4.8, #44), spec-mcp (la spécification elle-même : c'est une constante du code, tenue à la main, et spec_mcp.courante en en-tête la déclare comme telle) et mcp-scanner (non branché, §10.5).

auto-inscription prime sur la clé de la famille. Une entrée que la boucle s'ajoute elle-même porte une référence d'issue en colonne source, et elle se publie sous auto-inscription quelle que soit sa famille, sinon l'index de la famille se voit crédité d'une ligne que nous avons écrite. C'est le défaut corrigé par #29 pour MCP, revenu en #98 sur les marketplaces par une porte que #29 n'avait pas fermée.

Cette liste n'est plus écrite dans export.ts. Les clés d'index et de sondeur sont déclarées par la famille, dans src/familles.ts, et l'en-tête les assemble, ajouter une famille ne doit pas obliger à modifier l'exporteur (#90). Deux familles qui publieraient la même clé lèvent au chargement du module, plutôt que d'écraser silencieusement l'une des deux dans un fichier en ligne.

identite : nom, protocole, url, paquet, depot. mesure_le est la date de première vue au registre ; les champs sont rafraîchis à chaque ingestion, mais republier la date de la dernière ferait bouger toutes les lignes à chaque heure pour rien.

sante : etat parmi vivant | protege | mort | ambigu | non_testable, et portee, qui dit ce qui a été appelé :

porteeCe qui a été fait
appelun échange applicatif a eu lieu, handshake MCP joué (initialize, version négociée relue, §4.2), ou lecture de tâche JSON-RPC 2.0 chez un agent A2A (#90)
httple contenu a été parsé, sans échange applicatif, les registres shadcn (#21 §1), les fichiers llms.txt (#95), les marketplace.json (#98), les documents OpenAPI lus chez leur fournisseur (#102), les agentfacts.json (#105), et toute entrée qui répond autre chose que sa propre enveloppe
existencerien n'a été appelé

Absente, la clé dit qu'on n'est pas allé au-delà d'un code de retour, ou que le contrôle est antérieur au 24 août 2026. C'est un aveu assumé, et il ne se rétro-déduit pas : deviner la portée d'un ancien contrôle referait exactement le défaut D7 qu'elle corrige.

depuis date le début de l'état courant : ni premier_vu (une entrée ingérée il y a 60 jours et sondée ce matin n'est pas morte depuis 60 jours), ni la date du dernier changement (vivante il y a 40 jours et morte il y a une heure, ça fait une mort d'une heure). Même précaution que reclasse dans src/passage.ts.

Une entrée jamais sondée porte un motif, et il y en a trois :

"sante": {"etat": "non_testable", "portee": "existence",
          "motif": "paquet_seul_sans_url",
          "source": "registre", "mesure_le": "2026-08-13"}
motifCe que ça ditMesuré le 19/08/2026
paquet_seul_sans_urlverdict : paquet installable, aucune adresse à appeler. Elle ne s'interroge pas à distance, et ne s'interrogera jamais10 376
jamais_sondeaveu : l'adresse est joignable, nous ne l'avons pas encore sondée. Ça bougera au prochain passage11 838
ni_url_ni_paquetni adresse ni paquet : le registre la liste, il n'en donne aucun point de contrôle (55 n'ont même pas de dépôt)372
url_a_gabaritverdict : l'adresse publiée est un gabarit à trous (https://x.io/mcp?api_key={api_key}). Ce n'est pas une adresse, donc aucun verdict de santé n'est possible148 au 01/09/2026

Le quatrième motif est entré le 1er septembre 2026, et il retire des verdicts déjà publiés. Sur les 148 entrées à gabarit du fichier en ligne, 51 étaient publiées vivant et 53 mort : les deux sont faux de la même façon, et le second est le pire, https://{HAPI_FQDN}:{HAPI_PORT}/mcp n'est pas mort, il n'existe pas. Ces entrées ne sont plus sondées (src/passage.ts) et sortent non_testable même quand la base porte encore leurs anciens contrôles : une mesure ne s'efface pas, mais republier un verdict rendu sur une chaîne à trous serait exactement ce que ce projet reproche aux annuaires qu'il recense.

Les deux premiers ne sont pas la même chose et ne doivent pas se lire sous un seul chiffre : non_testable mêlait un verdict sur le corpus et un retard d'ingestion. compte.non_testable_par_motif les sépare en en-tête, et latest.md les affiche ligne par ligne, MOTIFS faisant foi (la page itérait une liste recopiée à la main : un quatrième motif sortait dans le JSON et manquait sur le tableau de bord). La troisième branche existe parce que 372 entrées portaient paquet_seul_sans_url sans avoir de paquet : l'étiquette affirmait un paquet installable qui n'existe pas (#29).

La glose se clé par motif, jamais par etat. C'est une clause du contrat, parce que la phrase n'est plus dans le fichier : les trois clients la portent en dur et doivent l'écrire à l'identique.

motifCe qu'un client affiche
paquet_seul_sans_urlexistence vérifiée, fonctionnement non testé
jamais_sondepas encore sondée
ni_url_ni_paquetaucun point de contrôle
url_a_gabaritadresse à trous, rien à appeler

Clée par etat, une table n'a qu'une entrée non_testable et affirme donc « existence vérifiée » sur les trois motifs, dont deux où rien n'a été vérifié. Et c'est précisément là que ça tombe : hultra-link etat <url> comme l'outil etat_serveur (§4.8) cherchent par identite.url, or un paquet_seul_sans_url n'en a pas, les seules entrées non_testable qu'ils atteignent sont les jamais_sonde.

compte.non_testable_par_motif publie TOUJOURS tous les motifs, zéros compris, y compris quand il n'y a aucune entrée non_testable, auquel cas les tous valent 0. Le bloc n'est donc jamais {} : le projet interdit le vide, et « absence = non mesuré » ne se négocie pas. Ces zéros-là sont bel et bien mesurés : les motifs sont exhaustifs et toujours calculables (motifNonTestable, src/export.ts), un motif à 0 est donc un compte, pas une lacune. Ne poser la clé qu'à la première occurrence faisait disparaître un motif nul, et un tiers ne pouvait plus distinguer « zéro mesuré » de « motif que cette version d'export ne connaît pas ». Un quatrième motif, s'il apparaît un jour, apparaîtra dans les trois clients, dans ce tableau et dans cette liste au même commit.

version : spec est la version négociée, jamais celle demandée (piège 2 du §4.2). retard se compte sur l'échelle de révisions que la famille déclare ; pour MCP c'est SPECS dans src/sonde.ts, et la version courante comme la liste se lisent en en-tête, sous spec_mcp, pas par entrée.

Le bloc est absent pour les registres shadcn : leur sondeur range dans la même colonne l'empreinte du corps refusé (#22), la publier comme une spec inventerait une mesure. Ce qu'une famille met dans cette colonne est déclaré dans src/familles.ts et non déduit de son nom : un test par nom de protocole aurait fait sortir toute famille ajoutée après coup comme si sa colonne était une version (#90).

Pour OpenAPI, spec est la version déclarée dans le document servi par le fournisseur, jamais l'openapiVer de l'annuaire, qui décrit sa copie convertie et date de 2023. retard s'y compte sur l'échelle publiée par OAI/OpenAPI-Specification (courante 3.2.0, 19 septembre 2025).

Une colonne dit UNE chose par famille. Les familles dont colonneSpec vaut empreinte y rangent le sha256 du corps refusé et ne publient jamais de bloc version ; celles dont elle vaut version n'y mettent que des versions : une empreinte y sortirait publiée comme une version de protocole (#102).

Pour A2A, spec est la version que l'agent déclare dans sa propre carte, lue au chemin conventionnel /.well-known/agent-card.json, jamais celle que le registre publie pour lui. C'est le piège 4 du §4.2 sous une autre famille : une version recopiée d'un tiers n'est pas une mesure. retard y est absent, et c'est voulu : le dossier ne porte aucune liste sourcée et datée des révisions A2A, et un 0 dirait « à jour ».

config : le bloc prêt à coller du §2.3, et la famille dit lequel. Il n'est publié que pour MCP, sous la forme { mcpServers: … }. Il sortait pour toute entrée à spec connue : un agent A2A en serait sorti avec une configuration de client MCP portant son URL, c'est-à-dire un bloc à copier qui ne marche pas, publié sur un tiers nommé (#90).

Une source d'amorçage n'est pas un registre, et la clé le dit : liste- plutôt que registre- ou index-. Pour ces familles, la couverture n'est jamais affichable en pourcentage, le dénominateur est une liste tierce, pas la population (PRD.md §4.13). Diviser par elle mesure la justesse de la liste, jamais la santé de la famille.

outils : le compte de ce que l'entrée expose, et depuis le 1er septembre 2026 les noms. Pour MCP, la longueur de tools/list.

Pourquoi les noms sont entrés. La sonde appelait tools/list et n'en gardait que la longueur : l'annuaire publiait « 23 outils » sans jamais dire lesquels, alors que les noms étaient dans sa main à la ligne où il les jetait. Un agent qui s'en est servi pour de bon le 1er septembre 2026 a donc ouvert trois sessions MCP et rappelé tools/list lui-même pour savoir si ces serveurs l'intéressaient. C'est un aller-retour que la donnée déjà mesurée rendait inutile.

Deux dates dans un seul bloc, et c'est voulu. n vient du dernier contrôle ; noms vient du dernier contrôle qui a obtenu une réponse de tools/list, et porte son propre mesure_le. Les fondre publierait une liste d'hier sous la date d'aujourd'hui. Un contrôle qui ne liste rien ne touche pas à la liste précédente, qui reste, datée de sa propre mesure.

La table outil porte un ÉTAT, pas une série (clé primaire (entree_id, nom)) : elle est effacée puis réécrite à chaque contrôle qui a obtenu une réponse, sans quoi un outil retiré resterait publié pour toujours, ce qui est la panne que cet annuaire mesure chez les autres. Contrairement à item_registre, elle n'historise donc rien.

Les noms sont bornés : 200 par entrée, 80 caractères chacun (src/sonde.ts). Ce sont des chaînes venues d'un tiers, republiées telles quelles ; un serveur qui en déclare 5 000 ne doit pas pouvoir décider du poids de notre publication. Le compte n, lui, porte sur la liste entière : c'est le seul endroit où les deux peuvent différer, et l'écart dit qu'il y a eu troncature.

etiquettes : une ou plusieurs étiquettes de domaine grossières, dérivées par appariement de mots-clés sur le nom et les noms d'outils relevés (src/etiquettes.ts). L'URL n'entre pas dans l'appariement : elle dit où le serveur est hébergé, pas ce qu'il fait. Mesuré par un juge le 1er septembre 2026, avant qu'elle en sorte : 294 entrées étiquetées cloud par le mot vercel, dont 284 où il n'apparaissait que dans l'URL, tunnels trycloudflare.com compris. Elles portent source: "etiquette", et l'en-tête déclare cette source comme dérivée, jamais vérifiée à la main : ce n'est pas un classement rendu par quelqu'un. Bloc absent quand rien n'est reconnu, jamais un "autre" qui dirait « examiné, classé nulle part ».

Elles existent parce que les noms du registre ne sont pas sémantiques : ils sont en DNS inversé (io.github.proprietaire/depot), et chercher « DNS et TLS » par expression régulière sur les noms et les URL rend des spécifications de certificats Azure (mesuré le 1er septembre 2026). Corollaire écrit dans le code et tenu par un test : aucun mot déclencheur ne peut attraper l'espace de noms du registre (github, gitlab), sous peine d'étiqueter « code » les deux tiers du corpus, météo comprise. Pour A2A, le nombre de compétences déclarées dans la carte de l'agent (skills[], spec A2A §4.4.1) : c'est là qu'A2A les publie, et aucun appel supplémentaire n'est fait chez un tiers pour remplir une colonne (§4.6). Absent quand la carte est absente ou muette, un 0 dirait « aucune compétence ». Pour llms.txt, le nombre de liens de l'index, et un 0 y est publié, parce qu'un index conforme et vide est sain : le format rend les listes facultatives. Un zéro mesuré est une mesure, l'absence est autre chose (#53, #95). Pour un marketplace Claude Code, le nombre de plugins déclarés (#98). Pour un agentfacts.json, le nombre de skills déclarés, ce que l'agent dit savoir faire, et non ses capabilities, qui disent comment il parle (#105). Pour un document OpenAPI, le nombre d'opérations (chemins × méthodes), le seul compte fidèle à « décrit des appels » : un document à 40 chemins et 200 opérations décrit 200 appels, pas 40 (#102).

mort porte sur L'ARTEFACT SONDÉ, jamais sur ce qu'il décrit. Une entrée nanda en mort dit que l'agentfacts.json publié par l'index ne répond pas, elle ne dit pas que l'agent est tombé. Mesuré le 25 août 2026 : sur 206 agents dont le document de faits ne répondait pas, 166 répondaient à leur point d'accès (#105). Confondre les deux, c'est publier une mort qu'aucune mesure ne soutient.

mort ne veut pas toujours dire « était vivant puis est tombé ». Pour les familles amorcées sur une liste déclarative, il veut dire « l'adresse publiée ne répond pas », ce qui inclut « n'a jamais répondu » : 31 dépôts portant le topic claude-code-marketplace n'ont aucun fichier de marketplace. Le verdict ne se plie pas pour une famille, la table est commune, mais CHIFFRES.md §9.7 porte la glose, et un réutilisateur qui compte des pannes doit la lire.

outils n'est PAS publié pour shadcn, et le contrat le disait déjà en n'énumérant que six familles. La sonde shadcn range pourtant le total de ses items dans la même colonne nb_outils, si bien que 255 entrées sur 255 publiaient outils.n et items.total avec la même valeur (mesuré en ligne le 1er septembre 2026) : une mesure publiée deux fois sous deux noms, ce que #29 et #31 ont chassé partout ailleurs. Une famille qui publie items ne publie plus outils.

items : les registres shadcn seulement (#55). Les compteurs par type, tels que le registre les nomme lui-même (registry:ui, registry:block, registry:component, …), plus un total. Aucun vocabulaire inventé, aucun regroupement : le regroupement lisible se fait à l'affichage.

Aucun tri n'est imposé, et c'est une décision datée (#54, 19 août 2026) : publier ces nombres ordonnés par volume serait un classement de gagnants, que le §2.2 refuse. Trier à l'affichage est une vue ; trier dans le fichier, c'est un palmarès dont hérite tout lecteur.

Trois cas, qu'il faut pouvoir distinguer :

SituationCe qui est publié
registre sondé, N itemsles compteurs par type, et total
registre sondé, 0 itemtotal: 0, explicitement
registre non sondébloc absent

Confondre les deux derniers ferait lire « vide » là où il faut lire « pas encore mesuré » : la faute exacte que #31 a retirée du bloc audit. Le cas n'est pas théorique : mesuré le 19 août 2026, shoogle.dev/r/registry.json rend 200, du JSON valide, un horodatage frais et zéro item (#53). La table de santé de #21 le déclare sain, ce qu'il est ; ce bloc dit ce qu'elle ne dit pas.

mesure_le date l'instantané. La table item_registre en garde la série (clé primaire (entree_id, mesure_le, type)) mais seul le dernier est publié : l'historique servira au diff « ce qui est apparu, ce qui a disparu » (#54 lot 2), qui se lit en base et non dans le fichier.

outils : n, et noms quand la liste a été relevée. audit : absent tant que mcp-scanner n'est pas branché, l'en-tête portant le « non exécuté » (voir ci-dessus) ; la place reste réservée dans l'ordre des clés pour le jour où il tourne. config : le bloc prêt à coller, produit seulement pour ce qui a réellement parlé MCP.

compte.par_palier compte le FICHIER, auto_mesure.par_palier compte la BASE

Les deux sont dans le même en-tête et ils ne donnent pas le même total : c'est voulu, et il fallait le dire. auto_mesure.par_palier est l'exception au périmètre sondable des fraîcheurs et de la couverture : il décrit toute la base, y compris les entrées que la publication écarte. compte décrit ce fichier, et lui doit s'additionner.

Mesuré le 1er septembre 2026 sur la publication en ligne : auto_mesure.par_palier sommait 31 481 quand compte.entrees valait 31 398, soit 83 lignes que personne ne pouvait trouver dans le fichier. Deux lecteurs indépendants ont pris par_palier pour une répartition des états et ont conclu à une incohérence des données. compte.par_palier existe depuis pour que l'arithmétique du fichier tombe juste, et le palier ne se confond pas avec l'état : palier.mort est une CADENCE (mort depuis plus de 30 jours, contrôlé une fois par semaine), etat.mort est un VERDICT. Ils n'ont aucune raison d'être égaux, et ils ne l'étaient pas (2 945 contre 3 032).

catalogue.json

Même en-tête, même forme, une entrée par ligne : c'est latest.json filtré, et rien d'autre. Deux coupes, dites par compte.filtre :

  1. sante.etat ∉ {vivant, protege} : les morts et les non testées sortent ;
  2. le bloc config, qui ne sert qu'au moment de brancher un serveur déjà choisi.

Il existe pour un usage que latest.json sert mal. latest.json est une base de données à recopier : un tiers qui reconstruit un état a besoin des morts, des non testées et des blocs de configuration. Un agent qui pose une question n'a besoin d'aucun des trois : le 1er septembre 2026, un agent a tiré les 31 398 entrées du fichier pour en garder dix. Le même en-tête, ce jour-là, comptait 11 635 non_testable, dont 11 231 paquet_seul_sans_url — des lignes sans aucune adresse à appeler, que ce fichier ne porte donc pas.

Son en-tête compte CE fichier, pas le corpus : compte.entrees est le nombre de lignes présentes, compte.par_etat ne porte que vivant et protege, et compte.corpus_entier dit combien il en manque et donc où aller les chercher. Un par_etat recopié de latest.json ferait une somme fausse, et un lecteur qui vérifie l'arithmétique de l'en-tête a raison de le faire.

Poids mesuré le 1er septembre 2026, en rejouant les deux coupes sur le latest.json publié ce jour-là (31 398 entrées, 13,38 Mo bruts, 1 031 Ko en gzip -9) : 16 282 entrées, 6,67 Mo bruts, 518 Ko en gzip -9, soit la moitié dans les deux unités. C'est une simulation du filtre sur le fichier publié, pas un export produit par la boucle : le premier vrai catalogue.json porte en plus les noms d'outils, entrés le même jour.

Ce que les noms d'outils vont coûter, et pourquoi ce n'est pas encore mesuré. Aucun passage complet n'a encore tourné avec eux : le parc se re-sonde en moins de 72 h, et le chiffre ne vaudra qu'après. L'ordre de grandeur, à partir des 124 666 outils comptés sur les 7 334 entrées MCP qui en déclarent (en-tête du 1er septembre 2026) et d'un nom à ~22 octets : +2,7 Mo bruts sur latest.json, à remesurer et non à recopier.

Ce n'est pas un second contrat. Les entrées y sont identiques à celles de latest.json, aux deux coupes près : rien n'y est reformaté, rien n'y est ajouté. Un client écrit pour l'un lit l'autre.

latest.md

Le tableau de bord, pas le catalogue : 21 844 entrées ne tiennent pas dans une page lisible. L'ordre suit le §2.2, ce qui est cassé avant l'état général : âge du contrôle le plus ancien, auto-mesure, ce qui a cassé depuis hier, ce qui est revenu, état par protocole, versions de spec MCP, fichiers. Les deux listes de changements sont coupées à 50 lignes, avec le compte de ce qui suit.

Sous le tableau par protocole, le détail de non_testable par motif : la glose disait « existence vérifiée, fonctionnement non testé » pour la colonne entière, alors qu'elle ne vaut que pour paquet_seul_sans_url. Les entrées jamais_sonde n'ont rien de vérifié, elles attendent leur tour.

Ce détail suit la même convention que le tableau par protocole, juste au-dessus : il publie ses zéros. Deux conventions opposées à dix lignes d'écart sur la même page font lire un motif absent comme une lacune de mesure ; ici comme en en-tête, un zéro est un compte. En revanche, le paragraphe et le tableau ne sont émis que si compte.par_etat.non_testable est non nul : sinon la page publiait un paragraphe qui annonce un tableau, suivi d'un tableau réduit à sa ligne d'en-tête. Une page ne publie pas un tableau vide ; l'en-tête de latest.json, lui, porte toujours les trois zéros, c'est lui le contrat machine.

llms.txt

L'index d'entrée des agents. Il porte le titre, l'attribution de licence, l'âge du contrôle le plus ancien et la couverture réellement mesurée sur 24 h, controlees_24h / joignables sur les entrées sondables, avec sa date. Il annonçait « toute adresse joignable est contrôlée au moins une fois par jour » : c'est l'objectif du §4.4, pas l'état courant. Un objectif écrit au présent dans le fichier que lisent les agents est exactement la faute que cet annuaire mesure chez les autres (#29).

Ce qui fait foi, c'est la valeur calculée au passage : le fichier n'écrit jamais un taux en dur, il divise controlees_24h par joignables à chaque export et date le résultat. Le seul chiffre cité dans ce dossier et dans les commentaires du code, 600 sur 12 530, soit 4,8 %, est celui relevé par #29 le 19 août 2026 ; il sert d'ordre de grandeur et de motif de la coupe, il n'a pas autorité sur la publication. Toute autre valeur qui circulerait (7,1 %, par exemple) n'est attestée nulle part dans ce dépôt et ne doit pas être reprise.

sante.json

Le bloc auto_mesure, seul, plus la licence. C'est ce que va chercher un badge, une barre de site ou un curl de surveillance : leur faire tirer 11 Mo pour afficher « 19,4 h » garantit que personne ne l'affichera. Un seul calcul (src/mesure.ts), deux emplacements.

Publication

R2 (#6), poussée par src/export.ts via l'API S3, voir deploiement/README.md. Le Content-Type se pose à l'écriture, y compris text/markdown pour les .md. latest.json est poussé en dernier : une poussée interrompue ne doit pas annoncer un état frais pointant vers des fichiers absents.