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 tablesourcesde l'en-tête (déclarée une fois pour tout le fichier : nom, URL, méthode). La clé vient de la colonneentree.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 avecsource: "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êtepassage. 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.
| Champ | Sujet | Relevé du 24 août 2026 |
|---|---|---|
dernier_passage_le | quand la boucle a sondé | il y a 1 min |
mesure_le | quand ce fichier a été construit | il y a 59 min (l'export est horaire) |
age_entree_la_moins_fraiche_h | l'entrée du parc vue il y a le plus longtemps | 33,8 h |
retard_max_h | de combien la plus en retard dépasse sa propre échéance | 0 |
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êtepassage, 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.txtannonç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.mdde 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-scannern'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 desante.motif. Coût mesuré le 19 août 2026 sur les octets qu'écritsrc/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 parmotifet non paretat, voir le tableau des motifs ci-dessous. La changer, c'est la changer dans les trois clients ET ici, etsrc/differentiel.test.tscompare les trois tables à chaque exécution.version.courante: c'étaitDEMANDEdesrc/sonde.ts, une constante du code, republiée 237 fois avecsource: "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, sousspec_mcp, avec sa vraie source (spec-mcp) et la listeconnues.version.retardreste par entrée : lui se compte sur la spec négociée par cette entrée, donc il mesure (#29).: publiés depuis le 1er septembre 2026, table annexeoutils.nomsoutil(voir le blocoutilsplus bas). Ce qui reste vrai : pas de clénomsvide, une entrée qui n'a jamais listé ses outils publienseul.- 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
.mdpar 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é :
portee | Ce qui a été fait |
|---|---|
appel | un é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) |
http | le 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 |
existence | rien 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"}
motif | Ce que ça dit | Mesuré le 19/08/2026 |
|---|---|---|
paquet_seul_sans_url | verdict : paquet installable, aucune adresse à appeler. Elle ne s'interroge pas à distance, et ne s'interrogera jamais | 10 376 |
jamais_sonde | aveu : l'adresse est joignable, nous ne l'avons pas encore sondée. Ça bougera au prochain passage | 11 838 |
ni_url_ni_paquet | ni 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_gabarit | verdict : 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 possible | 148 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.
motif | Ce qu'un client affiche |
|---|---|
paquet_seul_sans_url | existence vérifiée, fonctionnement non testé |
jamais_sonde | pas encore sondée |
ni_url_ni_paquet | aucun point de contrôle |
url_a_gabarit | adresse à 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 :
| Situation | Ce qui est publié |
|---|---|
| registre sondé, N items | les compteurs par type, et total |
| registre sondé, 0 item | total: 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 :
sante.etat∉ {vivant,protege} : les morts et les non testées sortent ;- 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.