Skip to content

garde : un module que seul son propre test consomme - #23

Merged
Palenza merged 5 commits into
travailfrom
arc-fidelite
Aug 2, 2026
Merged

garde : un module que seul son propre test consomme#23
Palenza merged 5 commits into
travailfrom
arc-fidelite

Conversation

@Palenza

@Palenza Palenza commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Le mode de panne

orchestrationRecovery.ts porte une machine de reprise complète — détection de trou de séquence, replay, backoff, abandon après N essais sans progrès. Quarante assertions la couvrent. Elle est verte à chaque CI.

Elle n'est appelée par rien depuis que « Rewrite client connection architecture » (pingdotgg#2978) a supprimé son unique consommateur. Tracé commit par commit : née avec un consommateur mobile → déménagée vers client-runtime en 87d12d72a → orpheline en pingdotgg#2978.

C'est pire que du code mort franc. Un lecteur — humain ou agent — ouvre le fichier, voit la logique et les tests verts, et conclut que le comportement est livré.

Pourquoi knip ne le voit pas

Pour un détecteur de code mort, un import est un usage — et le fichier de test en fait un. Ce garde pose l'autre question : « quelqu'un d'autre que son test l'importe-t-il ? »

La mesure

91 modules dans cet état sur les cinq zones scannées.

Un cliquet, pas un mur : la détection des points d'entrée n'est jamais parfaite (bin.ts n'est importé par personne, et c'est normal). On fige la liste du jour, on refuse qu'elle grandisse. Les faux positifs d'aujourd'hui sont neutralisés une fois pour toutes ; toute nouvelle apparition est un vrai signal.

Deux détails qui séparent un garde d'un décor :

  • un plancher de fichiers scannés — sous 500 on refuse de conclure, parce qu'un chemin déplacé rendrait « zéro orphelin », vert et mensonger ;
  • les imports sont résolus, jamais cherchés en sous-chaîne : « Suggestion » ⊂ « SuggestionPanel », et un premier jet à la sous-chaîne trouvait 33 orphelins là où la résolution réelle en trouve 91.

Preuve

Module jetable + son test → le garde sort 1 et le nomme. Retiré, le cliquet retombe à 91.

Ce que cette PR ne fait pas : supprimer les 91. Une suppression de fichier se confirme (D5), et plusieurs sont des points d'entrée légitimes.

vp check ✓ · typecheck ✓ · suite complète 2 812

🤖 Generated with Claude Code

Enzo and others added 5 commits August 3, 2026 03:12
Poser le périmètre d'un mode était typé `Effect<void, never>`. Deux pannes
réelles — settings.json abîmé, disque en lecture seule — tombaient dans un
`catch` qui journalisait et rendait `void`. L'appelant faisait donc
`appliques += 1` sans condition : il n'avait rien d'autre à regarder.

Ce n'est pas un défaut d'affichage. La bannière ambre affirme que les agents
ne peuvent NI écrire NI lancer de commande. La croire alors que rien n'a été
écrit, c'est lancer un agent en le pensant bridé. Le mode de panne d'un garde,
c'est de PROMETTRE une protection absente — pire que pas de garde, parce qu'un
garde absent ne rassure personne.

La pose rend maintenant son issue (`applique` / `settings-illisible` /
`ecriture-refusee`), et SAUTÉ cesse d'être confondu avec ÉCHOUÉ : un compte
sans dossier propre est attendu et n'appelle aucun geste ; un compte en échec
devait recevoir le périmètre et ne l'a pas reçu.

Le décompte et la phrase vivent désormais dans un seul module. Le bug est né
de leur dispersion : le serveur comptait dans une route HTTP, le client
reformulait dans un composant React — deux moitiés intestables qui ne se
rencontraient qu'en production.

Preuve par mutation, en deux temps. La première passe a montré que remettre le
bug d'origine ne rendait AUCUN test rouge côté serveur : le maillon du milieu
était nu. Après regroupement, le même mutant (« un échec compte comme un
succès ») fait tomber 4 tests, dont celui qui traverse compter → décrire.

Trois portes fermées côté pose : succès, fichier abîmé (le fichier de
l'utilisateur reste INTACT — l'écraser pour poser un périmètre serait un
remède pire que le mal), disque refusant l'écriture.

Gates : vp check ✓ · typecheck ✓ · suite complète 2 799 ✓

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es de l'utilisateur

Poser un mode ÉCRASAIT `permissions.deny` et `permissions.allow` en entier. Et
le mode Atelier, qui n'impose aucune restriction, ne produit donc aucune règle
— il tombait dans la branche « rien à restreindre » et SUPPRIMAIT le bloc
`permissions` complet.

Un utilisateur qui avait écrit ses propres refus (`Bash(rm:*)`, `Read(**/.env)`),
un `defaultMode`, des `additionalDirectories`, perdait tout en cliquant sur le
mode qui promet de ne rien restreindre. Sur le disque, définitivement. Le mode
le plus inoffensif du catalogue était le plus destructeur.

La règle qui referme ça tient en une phrase : ON NE RETIRE QUE CE QU'ON A POSÉ.
Elle s'applique sans marqueur — un marqueur mentirait dès que l'utilisateur
édite son fichier — parce que le vocabulaire de nos règles est CLOS : les refus
sont toujours des noms nus d'outils (neuf valeurs), les autorisations toujours
des `Edit(motif)` tirés d'un périmètre déclaré. Tout le reste lui appartient et
se recopie intact.

Trois conséquences, chacune tenue par un test :
· Atelier fusionne au lieu d'effacer — le bloc survit entier ;
· un mode restrictif AJOUTE ses refus aux siens au lieu de les remplacer ;
· lever le mode rend le fichier exactement à son état d'origine (aller-retour
  revue → documentation → libre, comparaison de valeurs).

La reconnaissance du mode suit : elle comparait les listes ENTIÈRES, ce qui ne
marche plus dès qu'on fusionne. Elle ne regarde plus que NOS entrées — sans
quoi l'écran aurait dit « aucun mode » sur un mode bel et bien posé.

Zone d'ombre assumée et documentée : une entrée écrite à la main identique à
l'une des nôtres nous est indiscernable. Le sens de l'erreur est le bon —
retirer une autorisation rend la CLI plus prudente, pas moins.

Preuve par mutation, trois mutants, tous tués : la suppression du bloc (2
rouges), l'écrasement des listes (3 rouges), la reconnaissance sans filtrage
(1 rouge).

Gates : vp check ✓ · typecheck ✓ · suite complète 2 804 ✓

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le scanner d'invisibles était quatorze caractères écrits à la main. Il ratait
le BLOC TAG (U+E0000–U+E007F) — le canal documenté de contrebande : chaque
lettre ASCII y a un jumeau strictement invisible, donc une consigne entière se
cache dans un titre de skill. Et il bannissait U+200D sans nuance, alors que ce
même caractère assemble les emoji.

Mesuré sur les 371 fichiers de skills réellement installés :

    bloc Tag                        0 occurrence   → gratuit à bannir
    U+FE0F                         26 occurrences  dans 21 fichiers SAINS
    U+200D                          4 occurrences  dans 1 fichier SAIN — déjà banni
    sélecteur collé à de l'ASCII    0 occurrence   → c'est LÀ qu'est l'attaque
    run de sélecteurs consécutifs   1 au maximum   → une emoji en porte UN

D'où deux familles au lieu d'une. SANS APPEL : aucun usage légitime, la seule
présence est une trouvaille — le bloc Tag y entre, mesuré à zéro. À CONTEXTE :
les assembleurs d'emoji, légitimes collés à un pictogramme et seuls. Bannir
U+FE0F en bloc aurait fait hurler le scanner sur vingt et une skills honnêtes,
et un garde qui crie au loup se fait débrancher — donc ne protège plus rien.

Une subtilité a mordu en écrivant le juge : dans `🏳️‍🌈`, le voisin de gauche du
jointeur n'est PAS le drapeau, c'est le sélecteur de variation. Juger sur le
voisin immédiat condamnait une emoji parfaitement normale. On remonte au
caractère de BASE, celui qui se voit.

La trouvaille dit maintenant OÙ : un humain ne peut pas chercher à l'œil un
caractère qui, par définition, ne se voit pas.

Preuve sur le parc RÉEL, pas seulement en unitaire :
    371 fichiers → 0 signalé   (l'ancien en criait un, sur U+200D légitime)
    piège bloc Tag → détecté

Gates : vp check ✓ · typecheck ✓ · suite complète 2 812 ✓

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…amais

Le champ annonçait, aux deux moteurs : « après ce nombre de minutes sans
dictée, le moteur est arrêté pour libérer la mémoire ». Sur le moteur LOCAL
c'est faux depuis le premier jour — `TranscriptionService` le construit avec
`idleTimeoutOverride: Duration.infinity` (GO fondateur du 29/07 : recharger
sept secondes à chaque reprise coûtait le premier clip). Le faucheur n'est donc
jamais lancé, et le modèle — ~600 Mo — reste en mémoire pour toujours.

La faute d'origine est une GÉNÉRALISATION, et elle est de moi : en câblant ce
champ j'ai cité le faucheur du SIDECAR (`TranscriptionService.ts:306`) pour
justifier une promesse faite aux deux branches. Un réglage lu par le serveur
n'est pas un réglage appliqué partout — il faut regarder la branche qu'on
emprunte, pas celle qu'on a sous les yeux.

Le comportement ne bouge PAS : garder le modèle chaud est une décision, pas un
bug. On cesse seulement de prétendre l'inverse — le texte dit désormais quel
moteur ce délai gouverne, et le champ est inerte là où il l'était déjà en
silence. Un réglage décoratif fait douter de tous les autres.

Reste à trancher par Enzo, hors de ce commit : le moteur local tient ~600 Mo
en permanence, y compris quand la dictée n'a pas servi de la journée.

Gates : vp check ✓ · typecheck ✓ · suite complète 2 812 ✓

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cliquet posé

`orchestrationRecovery.ts` porte une machine de reprise complète : détection de
trou de séquence, replay, backoff, abandon après N essais sans progrès. Quarante
assertions la couvrent. Elle est verte à chaque CI.

Elle n'est appelée par RIEN depuis que « Rewrite client connection
architecture » (pingdotgg#2978) a supprimé `packages/client-runtime/src/
environmentConnection.ts`, son unique consommateur. Tracé commit par commit :
le module est né avec un consommateur mobile, l'a vu déménager vers
client-runtime en 87d12d7, et l'a perdu en pingdotgg#2978. Il a survécu, ses tests
aussi, et plus personne ne l'exécute.

C'est pire que du code mort franc. Un lecteur — humain ou agent — ouvre le
fichier, voit la logique ET les tests verts, et conclut que le comportement est
livré. Il construira sa décision suivante là-dessus.

Knip ne le voit pas : pour un détecteur de code mort, un import EST un usage,
et le fichier de test en fait un. Ce garde pose l'autre question — « quelqu'un
d'AUTRE QUE SON TEST l'importe-t-il ? »

Mesure : 91 modules dans ce cas sur les cinq zones scannées. Un CLIQUET, pas un
mur : la détection des points d'entrée n'est jamais parfaite (`bin.ts` n'est
importé par personne et c'est normal), donc on fige la liste du jour et on
refuse qu'elle grandisse. Les faux positifs d'aujourd'hui sont neutralisés une
fois pour toutes ; toute nouvelle apparition est un vrai signal.

Deux détails qui font la différence entre un garde et un décor :
· un plancher de fichiers scannés — sous 500, on refuse de conclure, parce
  qu'un chemin qui a bougé rendrait « zéro orphelin », vert et mensonger ;
· les imports sont RÉSOLUS, jamais cherchés en sous-chaîne : « Suggestion »
  est contenu dans « SuggestionPanel », et un premier jet à la sous-chaîne
  trouvait 33 orphelins là où la résolution réelle en trouve 91.

Contre-épreuve : un module jetable + son test → le garde sort 1 et le nomme.
Retiré ensuite, le cliquet retombe à 91.

Ce que ce commit ne fait PAS : supprimer les 91. Une suppression de fichier se
confirme (D5), et plusieurs sont des points d'entrée légitimes.

Gates : vp check ✓ · typecheck ✓ · suite complète 2 812 ✓

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Palenza
Palenza merged commit b4a6a70 into travail Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant