v0.1.145
l'outillage : dix gardes de CI et leurs bancs, pas une ligne du code servi. Aucune migration, aucun
changement de contrat, aucun changement de comportement. Monter est sans effet visible ; ne pas
monter l'est tout autant.
Il est publié parce que main ne doit pas rester loin de ce que le registre sert, pas parce qu'il
apporte quelque chose à qui l'installe.
règle qui manquait. Les neuf autres corrigent la façon de prouver une règle déjà écrite — un
analyseur absent du chemin du rouge, un plancher aveugle aux disparitions, une discrimination qui
tenait au tri alphabétique, cinq gardes qui affirmaient une absence sans pouvoir distinguer « rien
trouvé » de « rien regardé ». Aucune n'était visible depuis la précédente.
Et cinq fois sur ces quatre jours, le remède existait déjà dans ce dépôt — écrit, commenté, et non
appliqué à l'endroit d'à côté. Une garde bien écrite explique son mécanisme, et cette explication
est ce qui rend le fichier crédible : on ne rouvre pas la phrase qui justifie l'outil.
Changed
-
⚠️ Les trois dernières gardes d'absence portent un témoin — et la mesure a imposé un mécanisme
différent de celui des deux précédentes.secrets-en-clair,renvois-par-positionet
portes-de-reponseaffirment chacune une absence sur un large périmètre. Aveuglées, mesuré : les
trois imprimaient leur résumé complet et sortaient 0.⚠️ La recette des deux gardes précédentes ne marche pas ici, et c'est la garde qui l'a dit. Un
témoin dérivé — « au moins un corps écrit sur place » — refuse sur un dépôt sain : zéro
corps reconnu pour onze.end(bruts, parce que tout passe par le module des portes. Il aurait
exigé la chose même que la garde décourage.Un témoin dérivé n'est possible que si la forme correcte est une chose que le dépôt est censé
CONTENIR. Un blocpermissions:et un appel au module crypto le sont ; un secret, un renvoi par
numéro de ligne et un corps sans type ne le sont pas. Pour ceux-là il faut fabriquer le cas :
poser un exemplaire fautif, vérifier que la sonde le VOIT, le jeter.⚠️ Ce mécanisme n'était pas neuf ici. L'étape RLS deci.ymlle pratique depuis des semaines
sur les politiques Postgres — « on pose une politique dont on sait qu'elle existe, on vérifie que
la sonde la VOIT, et on l'enlève. Sans ce détour, le zéro qui suit ne prouverait rien. » Il
n'avait jamais été porté jusqu'aux outils. Cinquième fois que le remède existe ici, inutilisé.⚠️ Et le témoin des secrets est assemblé à l'exécution, jamais écrit. Cette garde balaie 104
fichiers detools/, le sien compris : un faux identifiant en clair y serait signalé par elle-même,
on l'exempterait, et l'exemption deviendrait le trou que son propre en-tête décrit. Un banc vérifie
qu'aucun fichier suivi ne porte ce littéral.Pour
portes-de-reponse, le témoin distingue voir de juger : une sonde qui reconnaît la
forme mais ne la juge plus fautive laisse passer exactement ce que la garde attrape, et un témoin
qui ne vérifierait que « vu » ne le dirait pas. -
⚠️ Deux gardes qui affirment une ABSENCE portent désormais le témoin de leur RÈGLE, pas seulement
celui de leur périmètre. L'idée vient de la session STUDIO, qui a trouvé la même chose chez elle
sur 97 fichiers : notre témoin d'exception prouve qu'une exception a encore un sujet, celui-ci
prouve que la règle en a encore un. Deux moitiés de la même précaution.permissions-workflowsaffirme « aucune écriture à la racine » sur neuf fichiers ;
liaison-de-cryptoaffirme « aucun appel sur le global » sur trente et un. Leur panne la plus
probable — une sonde qui ne reconnaît plus la forme — produit elle aussi une absence. Le plancher
qui existait compte les FICHIERS LUS, jamais la FORME RECONNUE, et ne peut donc pas les distinguer.Mesuré en aveuglant chaque sonde :
avant : permissions : 9 workflows, aucune écriture à la racine code 0 après : GARDE NON CONCLUANTE — aucun bloc « permissions: » reconnu… code 2⚠️ Et la nuance, qui change la sévérité et qu'il serait malhonnête de taire : leurs bancs, eux,
attrapaient déjà la sonde aveugle. Ce n'était donc pas une garde morte — la RÈGLE était protégée.
C'est le verdict imprimé qui ne l'était pas, et c'est lui qui va dans le journal de la forge et
sous les yeux de quiconque lance l'outil à la main. La ligne comptait le périmètre et se lisait
comme une mesure.Les deux résumés disent maintenant ce qu'ils ont reconnu : « 9 bloc(s) lu(s) dans 9
workflow(s) », « 5 fichier(s) appellent le module parmi 31 ». Plancher à un dans les deux cas —
le compte du jour serait collé au relevé du jour.Pour
liaison-de-crypto, ce témoin est distinct de celui qui existait déjà :
methodesDuModuleSeul()refuse quand plus aucune méthode ne sépare le module du global, donc il
prouve que la question a encore un sens sur ce Node ; le neuf prouve que la sonde sait
encore lire la réponse. Deux cécités différentes, deux refus différents. -
⚠️ Deux bancs choisissaient leur cible par l'ORDRE DE TRI d'un dossier — la dette la plus vieille
de la série, soldée par construction plutôt que par mesure. Elle était signalée depuis trois
messages sans jamais avoir été cherchée.Le balayage n'a trouvé aucune nouvelle instance du défaut, et c'est la mesure qui le dit :
chaque candidat a été essayé, un par un. Dépouiller n'importe laquelle des deux images node
rougit ; abaisser n'importe laquelle des dix déclarations littérales rougit. Le vert n'était
pas un accident.Mais « mesuré aujourd'hui » et « ne peut pas dépendre du tri » ne sont pas la même affirmation :
la première a une date, la seconde n'en a pas. Les deux bancs bouclent donc désormais sur tous
les candidats. Personne ne relit un banc quand il ajoute un fichier.⚠️ Et le balayage a trouvé autre chose : un banc portait DEUX propriétés dans un seul test, ce
qui les affaiblissait toutes les deux. « Une étape dépouillée est refusée » vaut pour n'importe
laquelle — il ne faut donc pas en choisir une. « La mutation discrimine PAR ÉTAPE de PAR FICHIER »
n'a de sens que sur une cible dont le fichier garde d'autres déclarations — il faut donc en
choisir une, et délibérément. Fondues, la première héritait d'un choix dont elle n'avait pas
besoin, et la seconde d'un choix qu'elle ne faisait pas.Mesuré : trois de nos dix déclarations littérales vivent seules dans leur fichier, et sur
celles-là la discrimination ne tient pas. Le tri décidait donc si le banc prouvait sa seconde
propriété. Les deux sont maintenant séparées, et la seconde choisit sa cible pour ce qu'elle est. -
⚠️ Chaque exception écrite doit prouver qu'elle a encore un sujet — une entrée morte est une
porte ouverte d'avance. La distinction vient de la session STUDIO : une liste de ce qu'il faut
REGARDER cesse de couvrir dès qu'un fichier apparaît ; une liste de ce qui est PERMIS fait rougir
tout fichier qui n'y est pas. Nos deux listes de ce qui est permis étaient donc de la bonne forme
— mais tenues dans un seul sens.FICHIERS_MITdéclaresrc/bridge.tshors de l'AGPL. Le banc affirmait que le nom est dans
l'ensemble, jamais que le fichier existe. Le jour où il est renommé, l'entrée survit — et un
futur fichier à ce chemin exact serait relicencié MIT sans décision, alors que l'en-tête de la
liste dit qu'ajouter un fichier ici « se discute dans une PR, pas dans un correctif de garde ». Le
relicenciement se ferait par omission, sur une frontière de licence, garde verte.INTERNES_TOLERESdit « tout nouveau venu doit être décidé plutôt que découvert ». Cela n'était
tenu que contre les arrivants : un symbole qui cesse d'être exporté laissait son entrée
derrière lui, et son retour aurait été toléré au lieu d'être décidé.⚠️ Le remède existait déjà chez nous, inutilisé. La garde qui vérifie qu'aucune autre ne
déclare victoire sur zéro porte exactement ce patron depuis des semaines : chaque exemption a sa
raison et une fonction qui rougit quand le motif disparaît. Mécanisme inventé, commentaire
écrit, et non appliqué aux deux endroits où il manquait.⚠️ Et les éprouvettes ont corrigé la conception. Le contrôle des licences était d'abord dans
l'outil ; deux bancs ont rougi aussitôt, à juste titre —garde(racine)s'applique à une racine
QUELCONQUE et les éprouvettes lui passent des dépôts temporaires, tandis queFICHIERS_MITest
une constante du VRAI dépôt. Il accusait chaque éprouvette de ne pas contenirsrc/bridge.ts. Un
contrôle d'exception appartient là où le SUJET est connu : dans le banc pour les licences, dans
l'outil pour la surface publique, dont le sujet est toujours le module réel. -
⚠️ Le périmètre d'images-epingleesvient du disque — c'était une liste écrite, dans la garde où
ça coûtait le plus cher.ci.ymllui passaitDockerfile .zap/Dockerfileen dur. Cette garde
est la règle qui empêche une image de changer sous nos pieds : le jour où quelqu'un ajoutait un
troisième Dockerfile, elle aurait rendu « toutes épinglées » en n'ayant regardé que deux fichiers
sur trois. Son refus « zéro image » ne l'aurait pas dit — il compte ce qu'il a LU, il ne sait pas
ce qu'il n'a pas OUVERT.Le périmètre est désormais
git ls-files, partagé avecnode-de-l-imageplutôt que dupliqué :
deux exemplaires de « quels Dockerfiles existe-t-il ? » divergeraient. Un banc interdit à un
workflow de le remplacer par des arguments — dériver un périmètre ne sert à rien si un appel
l'écrase.⚠️ Et ce lot a réintroduit, en le déplaçant, le défaut queresultat-gardeexiste pour
interdire. Le calcul du périmètre vivait AU-DESSUS detenter, ce qui était sans conséquence
tant qu'il valait["Dockerfile"], une constante. Devenu une lecture du disque, son exception
a cessé d'être rattrapée : hors d'un dépôt git, l'outil mourait sur une trace de pile et sortait
1 — « corrige ta branche » pour un environnement sans git, exactement le rouge que la taxonomie
sépare. Les quatre mutants de ce lot et ses cinquante-trois bancs étaient verts ; seule
planchers-des-gardesl'a dit, en lançant l'outil dans un dépôt vide. Le calcul est passé
dedans, et la frontière est maintenant commentée à l'endroit où on la franchit.
Added
-
⚠️ La seconde moitié de la paire : ce que l'IMAGE embarque, confronté àengines. La première
garde le confronte à ce que la forge installe ; rien ne le confrontait à ce qu'un
auto-hébergeur exécute. Des deux exemplaires non mesurés, celui-ci était le plus proche de la
production.⚠️ Trois gardes touchent déjà ce fichier, et aucune ne pose cette question — relevé plutôt que
supposé :images-epingleesdemande « l'image vient-elle d'un condensat ? » ;ecartMajeur
demande « l'étiquette dit-elle vrai sur le condensat ? » ; le jobdockerconstruit l'image et
lui demande sa version. Le jour oùenginespassera au-delà de 24, les trois resteront
vertes : l'image serait épinglée, son étiquette dirait vrai, et elle embarquerait un moteur que
notre propre paquet déclare non supporté. Deux exemplaires cohérents entre eux ne disent rien du
troisième fait qu'ils ignorent.tools/node-de-l-image.mjsprend son périmètre sur le disque (git ls-files, jamais une liste
écrite), importe le lecteur de Dockerfile existant plutôt que d'en écrire un quatrième — celui
de ce dépôt a déjà été aveugle une fois — et porte la même relation que sa jumelle : toute image
qui fournit node doit dire laquelle.FROM node@sha256:…sans étiquette est valide, épinglé,
et sa version est indéterminable ; un comptage y verrait une image de plus, la relation y voit une
dette. Deux absences restent distinctes : rien de déclaré est une violation, une étiquette que
semver ne lit pas est non concluant.⚠️ Et ce que le resserrement écarte est RENDU.Dockerfile.proden est un,Dockerfile.mdest
une page qui en parle, et aucune lecture du nom ne les sépare sans convention. La convention est
donc écrite — les extensions de document sont écartées — et les fichiers écartés sont dits en
avertissement, parce qu'un resserrement muet est ce qui a coûté trois lecteurs à ce dépôt. -
⚠️ La relation est PAR ÉTAPE, et le banc cesse d'en avoir la preuve par accident. La session
STUDIO est tombée le 31/08 dans un trou que son dépôt lui cachait : sa garde comptait des
fichiers là où il faut compter des étapes, et comme aucun de ses fichiers ne portait deux
setup-node, les deux formulations y étaient observationnellement identiques. Aucune mutation
ne pouvait les distinguer — ses quatre mutants mouraient tous correctement, en éprouvant la règle
écrite plutôt que la règle voulue.Vérifié chez nous plutôt que supposé : notre règle est par étape (deux étapes dont une muette
→ 2 installations, 1 refus). Le trou n'est pas dans la règle. Il est dans le banc.Notre mutation sur fichier réel vise
declarations.find(…), c'est-à-dire l'ordre de tri du
dossier : elle ne séparait « par étape » de « par fichier » que parce que cette cible tombe dans
ci.yml, qui porte quatre étapes. Si elle avait trié danscla.yml— une seule étape — les deux
règles auraient rendu exactement le même vert. Une discrimination qui dépend du nom des fichiers
est un vert juste pour une mauvaise raison.Le cas est donc fabriqué : deux étapes, une seule déclaration, et le banc affirme qu'une règle
par fichier serait verte là où la nôtre refuse. La mutation réelle, elle, dit désormais ce
qu'elle discrimine au lieu de le supposer. S'y ajoute le défaut inverse, que le STUDIO a écrit pour
nous faute d'avoir des matrices : une étape qui tient sa version de${{ matrix.node }}déclare
— la compter muette accuserait le dépôt sain, et une garde qui crie faux finit desserrée. -
⚠️ La garde de node porte une RELATION, plus seulement des planchers — et c'est le correctif d'un
autre dépôt qui a désigné le trou chez nous. La session STUDIO a trouvé, dans sa propre garde, des
planchers collés au relevé du jour, et les a remplacés par un périmètre dérivé du disque. Passée
sur nos fichiers, l'idée a mesuré ceci : 11 étapesactions/setup-node, 11 déclarant une version,
et rien qui garde ce rapport.Un plancher compte ce qu'il voit ; il ne sait pas ce qui aurait dû être là. Retirer l'entrée
node-versiond'une étape faisait passer le relevé de 12 à 11 déclarations — au-dessus du plancher
de 8 — et la garde restait verte pendant que la forge installait le défaut de l'action, que rien
n'épingle et que rien ne confronte àengines. La lecture sautait d'ailleurs toute étape sans bloc
with:, donc la forme la plus dépouillée était aussi la plus invisible.La règle est donc : toute étape qui installe node déclare laquelle, périmètre pris sur le disque.
Elle ne remplace pas les planchers, elle les complète — un plancher garde la SONDE (« ai-je lu le
dossier ? »), la relation garde le SUJET (« chaque site d'installation est-il déclaré ? ») — et elle
porte le sien, parce qu'une relation peut être satisfaite à vide : zéro installation relevée,
aucune manquante, règle vraie pour n'avoir rien regardé.⚠️ Ce plancher-là s'est justifié le jour même, chez son auteur :garde()déstructurait encore
deux champs et ne transmettait pas le relevé neuf au verdict. Rien d'autre ne l'aurait dit.⚠️ Et l'ordre des contrôles est une décision, pas une mise en page. La relation est évaluée
APRÈS les planchers de déclarations : si la lecture desnode-versioncassait, les onze
installations paraîtraient toutes muettes et la garde rendrait onze violations — « corrige ta
branche » pour une panne qui n'y est pas. Un banc éprouve cet ordre, et le mutant qui l'inverse ne
fait rougir que lui. -
⚠️ La garde de node est éprouvée dans les DEUX sens — et le second attrape ce qu'aucun des vingt-trois
autres bancs ne voyait. L'idée vient de la session STUDIO, qui a retiré une exception de sa propre
garde le 31/08 et a nommé ce qui manque à une mutation unique : « sans cette seconde mutation,
j'aurais eu une suite verte parfaitement compatible avec j'ai supprimé un test gênant ».Ce qui manquait ici était plus précis que « un sens sur deux ». Toutes nos violations étaient
éprouvées sur des déclarations fabriquées, passées directement au verdict : l'analyseur n'était
jamais dans le chemin du rouge, seulement dans celui du vert. Le sens neuf part donc du texte
d'un vrai workflow — une version littérale du dépôt abaissée à node 18, relue par le vrai
analyseur, refusée en nommant le vrai fichier.Mesuré : un analyseur muté pour ignorer le texte qu'on lui passe et relire le disque fait
rougir ces deux bancs neufs et eux seuls — les vingt-trois autres restent verts. C'est la
preuve que le trou existait.S'ajoutent la partition énoncée d'un seul tenant — vert au milieu, rouge des deux bords, donc
aucun vert atteignable par accident — et un banc qui vérifie qu'un workflow lance vraiment cette
garde. Ce dernier ne protège pas la règle : le banc lit.github/workflowspour de vrai, donc
retirer l'étape deci.ymlla ferait rougir quand même — vérifié, contre une première rédaction qui
affirmait le contraire. Il protège la seconde jambe : la règle est tenue à deux endroits, c'est
délibéré, et une redondance non énoncée est précisément ce qui a fait survivre un mutant dans ce
même fichier. -
⚠️ La CI mesure enfin la version de node qu'elle installe — la garde voisine avait écrit ce
diagnostic le 25/08 sans se l'appliquer. L'en-tête detools/plancher-de-node.mjsdit :
«node-version: "22"résout au DERNIER 22.x : la CI atterrit toujours au-dessus du plancher,
quel qu'il soit. Une règle que l'environnement de vérification satisfait par construction n'est
pas vérifiée — elle est supposée. » Cette phrase nomme deux paires ; l'outil n'en mesurait
qu'une,enginescontre les dépendances de production. L'autre —enginescontre ce que les
workflows demandent d'installer — restait sur la prose qui venait de la diagnostiquer. Vérifié
plutôt que supposé : dans touttools/, la chaînenode-versionn'apparaissait qu'une fois,
et c'était dans ce commentaire.tools/node-des-workflows.mjsénumère les workflows depuis le disque — une liste écrite
cesse de couvrir dès qu'on ajoute un fichier — lit chaquenode-version:, résout
${{ matrix.node }}et les entrées d'include, et refuse toute version qu'enginesn'admet
pas. Relevé du jour : 12 déclarations dans 5 fichiers, toutes admises. Le rouge qu'elle attend
n'existe pas encore : il arrivera le jour où le plancher passera au-delà de 22, et ce jour-là
check (22)validerait en vert sur un moteur que notre propre paquet déclare non supporté.La comparaison est
intersects, passubset, et la différence est le cœur de la garde :
subset("22", ">=22.13.0")est faux — 22.0.0 est dans « 22 » sans être dansengines— donc
une garde bâtie dessus refuserait la CI d'aujourd'hui, qui est saine.intersectsest exact ici
sous une condition que la garde vérifie au lieu de la supposer : unenginessans plafond.
Sous unenginesborné en haut, elle refuse de conclure plutôt que de rendre un vert dont elle
ne sait plus ce qu'il vaut. Deux planchers anti-vacuité — sur le nombre de déclarations et
sur le nombre de fichiers, parce qu'un plancher unique laisse passer un balayage qui garde le
compte en perdant la moitié du dossier. Ce qu'elle ne sait pas lire (node-version-file,
lts/*, une expression non résolue) est dit, jamais sauté.
What changed in the package, by zone — 0.1.144 → 0.1.145
Measured on the two tarballs, by the release workflow. Not written by hand.
| Zone | What it is | Added | Removed | Changed |
|---|---|---|---|---|
documents |
what a human reads | 0 | 0 | 0 |
manifest |
package.json — version, exports, dependencies | 0 | 0 | 1 |
server |
the code the host executes | 0 | 0 | 0 |
context |
the injected-context implementations | 0 | 0 | 0 |
browser-types |
the declarations the host's tsc reads for « ./bridge » — breaks a build, never a page | 0 | 0 | 0 |
browser |
what the visitors' page executes | 0 | 0 | 0 |
cli |
the command-line entry point | 0 | 0 | 0 |
types |
declarations for the server and context entry points — breaks a build, never runtime | 0 | 0 | 0 |
database |
the schema and the migrations the host applies itself | 0 | 0 | 0 |
The files themselves
~ package.json