D-051 : une exception est du code ou de la configuration, jamais un commentaire #114

Merged
nicolas-fryder merged 8 commits from nicolas-fryder/dev into main 2026-10-06 10:02:29 +00:00 AGit

Les trois contrôles à marqueur (http.fetch-brut, config.lecture-hors-module, telemetry.console) sont retirés avec leurs marqueurs // eqs-*:autorise. Les règles passent dans les profils ESLint de @eqs/tooling (fetch nu, eqs/process-env, no-console strict côté serveur) ; l'exception à un endroit est un bloc files de configuration (endroits exportés), l'exception à un appel est une API nommée : appel(url, init, { horsContrat }) dans @eqs/http, urlDeConnexion dans @eqs/platform-next, environnementPourSousProcessus dans @eqs/config (les secrets de l'application ne partent plus vers PDAL et CloudCompare). Aussi : tooling.eslint-atteint, bloquant (#100) ; l'audit compte via tooling.fetch-nu et tooling.console et rend null pour une règle que la version installée ne déclare pas. Rien n'est publié : 15 changesets en attente. Porte verte. 🤖 Generated with Claude Code

Les trois contrôles à marqueur (http.fetch-brut, config.lecture-hors-module, telemetry.console) sont retirés avec leurs marqueurs // eqs-*:autorise. Les règles passent dans les profils ESLint de @eqs/tooling (fetch nu, eqs/process-env, no-console strict côté serveur) ; l'exception à un endroit est un bloc files de configuration (endroits exportés), l'exception à un appel est une API nommée : appel(url, init, { horsContrat }) dans @eqs/http, urlDeConnexion dans @eqs/platform-next, environnementPourSousProcessus dans @eqs/config (les secrets de l'application ne partent plus vers PDAL et CloudCompare). Aussi : tooling.eslint-atteint, bloquant (#100) ; l'audit compte via tooling.fetch-nu et tooling.console et rend null pour une règle que la version installée ne déclare pas. Rien n'est publié : 15 changesets en attente. Porte verte. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`tooling.eslint-etend` vérifiait que `eslint.config` importe les profils du
socle, jamais qu'elle touche un seul fichier. C'est la seconde moitié de #100.

Mesuré sur les dépôts tels qu'ils sont, le 2026-10-06 :

    eqs-satellite-template   13 fichiers sur 13 sous src/ hors d'atteinte
    eqs-demo                 42 fichiers sur 42

L'application de référence, celle dont le bac à sable valide chaque version,
n'avait jamais fait passer une seule de ses sources par ESLint.

`tooling.eslint-atteint` demande à ESLint, chargé depuis le dépôt inspecté, s'il
a une configuration pour chaque fichier TypeScript des dossiers `src/`. Il ne
lint rien, il résout. Bloquant : c'est une porte qui certifie ce qu'elle n'a pas
regardé, pas une convention mal suivie.

## Le piège qui aurait rendu le contrôle aveugle

Dans ESLint 9, `isPathIgnored` rend `true` pour un fichier qu'aucun bloc ne vise,
comme pour un fichier exclu exprès. Mesuré : avec le défaut de #100, `main.ts`
sort « ignoré » au même titre que `dist/main.d.ts`. Commencer par écarter les
fichiers ignorés aurait écarté `main.ts` et rendu vert sur du vide — la faute
même que ce contrôle refuse. Le tri se fait par le chemin.

## Deux instruments en retard sur le code, trouvés en chemin

- le banc de `@eqs/tooling` appelait ses contrôles sans `await` : le premier
  contrôle asynchrone y sortait en `{}`. `eqs doctor` attendait déjà chaque
  contrôle ; c'est le banc qui était en retard ;
- le garde de déclaration de `@eqs/testing` cherche le littéral
  `regle: '…'` dans le source. Une propriété raccourcie `{ regle, … }` lui
  échappe. J'ai écrit l'identifiant en clair plutôt que d'élargir le garde — mais
  c'est encore un instrument qui lit du texte, de la famille que la décision
  suivante s'apprête à retirer.

Éprouvé sur les vrais dépôts, puis par le banc dans les deux sens : un dépôt
sain ne rend rien, la copie fautive de `porter` est refusée en bloquant, avec le
compte et les fichiers.

Closes #100

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trois règles du socle détectaient par expression régulière ligne à ligne, et leur
exception était un commentaire `// eqs-*:autorise`. #110 a montré que Prettier le
déplace ; #112 a rendu la lecture plus habile au lieu de remettre en cause le
support. Un commentaire est la partie du code que le langage promet d'ignorer :
lui faire porter du sens, c'est accepter qu'une faute de frappe ne fasse rien,
qu'un formateur le déplace, et que rien ne dise qu'il ne sert plus.

Mesuré sur la flotte : dix exceptions posées, aucune vraie exception de ligne.
Cinq endroits, cinq trous du socle, un marqueur mort, une copie de la règle chez
`nuage`, et un besoin caché que l'arbre syntaxique a trouvé.

La décision, et trois arbitrages pris en mesurant (console, `NEXT_PUBLIC_*`, cible
déclarée), sont dans `reference/decisions.md`. Elle ouvre une suite de commits :
les trois API, les règles dans ESLint avec leurs endroits, puis le retrait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Premier des trois trous du socle que D-051 comble. Un appel sans contrat
OpenAPI s'écrivait `fetch(url) // eqs-http:autorise` ; il s'écrit maintenant

    await appel(url, init, { horsContrat: 'tuiles IGN publiques, sans contrat' })

La raison est une donnée obligatoire (vide, elle lève avant tout appel). Une
cible déclarée est refusée : `appel` ne contourne pas un contrat. Et l'exception
s'observe en production — le span porte `eqs.hors_contrat`, l'hôte et le chemin.

## Deux choix, et pourquoi

**La sémantique de `fetch`, pas celle du mutator.** Le mutator lit du JSON,
refuse les redirections et plafonne la taille : c'est le transport d'un contrat.
Les appels hors contrat mesurés dans la flotte sont des téléchargements de tuiles
LiDAR binaires. `appel` rend la `Response` brute, si bien que migrer un `fetch`
nu est un renommage, rien de plus.

**Pas l'URL complète sur le span.** Une API tierce met parfois sa clé dans la
requête ; la poser sur un span l'enverrait au collecteur. Un test le fixe en
posant `?cle=SECRET` et en exigeant qu'elle n'apparaisse nulle part.

Six tests, éprouvés en cassant le refus de cible puis l'exclusion de la requête :
chaque fois, exactement le test qui fixe la propriété rougit.

Noté pour l'étape du retrait : le cahier dit que le compte de `http.fetch-brut`
est remonté dans `eqs-audit.json` (`outgoing.rawHttpCalls`). La vue de flotte le
consomme ; il faudra le remplacer, pas seulement l'effacer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deuxième trou de D-051. Un satellite réécrivait l'appel à better-auth en `fetch`
nu pour obtenir l'adresse de connexion, parce que `SignIn` l'enfermait dans un
composant qui navigue. La fonction rend l'adresse et ne navigue pas ; `SignIn`
s'en sert, si bien qu'il n'y a qu'un seul chemin vers better-auth.

Elle corrige un défaut du composant au passage : il lisait le corps sans
vérifier le statut, donc un refus du serveur s'affichait comme une indisponibilité.

## Une crainte infondée, et une mesure fausse avant la bonne

`nuage` justifiait sa réécriture par un bundle navigateur pollué : le point
d'entrée client tirerait `node:async_hooks`. Ma première mesure lui a donné
raison — elle suivait les imports par expression régulière sur le texte, et elle
a pris pour un import une phrase de COMMENTAIRE contenant
`import { ApiError } from '@eqs/http'`.

Refaite par l'arbre syntaxique de TypeScript : ni `@eqs/platform-next/client` ni
`@eqs/http/browser` n'atteignent un seul module `node:`. La crainte était
infondée, et mon instrument venait de commettre exactement la faute que D-051
retire du socle — lire un commentaire comme s'il portait du sens.

Le nom diffère de celui annoncé (`seConnecter`) : une fonction qui rend une
adresse sans connecter personne ne peut pas s'appeler ainsi.

Quatre tests, le parcours social n'en avait aucun. Éprouvé en retirant la
vérification du statut : le test du refus rougit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Troisième trou de D-051, et le seul que les commentaires cachaient au point de
ne pas en porter. Un satellite passait `env: { ...process.env }` à deux binaires
géospatiaux tiers, avec un commentaire disant que c'était voulu. Pour le système,
oui. Pour `AUTH_SECRET`, `MS_CLIENT_SECRET` et `DATABASE_URL`, qui partaient avec,
personne ne l'avait décidé — et rien ne le montrait.

`environnementPourSousProcessus` rend l'environnement du système moins la
configuration de l'application : les clés du schéma sont retirées. Un secret
qu'un enfant doit vraiment recevoir s'ajoute par `ajouter`, écrit dans le code.

C'est la forme exacte de ce que D-051 dit : l'exception est portée par un nom de
fonction, cherchable, typé, et qui dit ce qu'il fait. Ici, en plus, le nom force
une frontière que le commentaire laissait ouverte.

Une directive `eslint-disable` posée par réflexe dans le socle a été retirée
avant le commit : la règle n'existe pas encore, et le module de configuration
est un ENDROIT autorisé à lire l'environnement — ce sera de la configuration à
l'étape suivante, pas une directive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les trois règles quittent les contrôles qui lisaient le texte ligne à
ligne pour les profils ESLint de @eqs/tooling : `no-restricted-globals`
et `no-restricted-properties` pour `fetch`, une règle propre
`eqs/process-env` (nest : toute lecture ; front : sauf NEXT_PUBLIC_* et
NODE_ENV sous forme littérale), `no-console` strict côté serveur.

Les exceptions sont des endroits, exportés sous `endroits` (tests,
scripts, démarrage, configuration) et relâchés par configuration. Chaque
paquet du socle déclare les siens dans son eslint.config : zéro
signalement restant, tous des défauts injectables ou de l'outillage.

Le banc du paquet joue treize fichiers témoins dans les deux sens ; il a
attrapé le piège mesuré de l'héritage d'options (`'no-console': 'error'`
gardait le `allow` de base).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`http.fetch-brut`, `config.lecture-hors-module` et `telemetry.console`
partent avec leurs marqueurs `// eqs-*:autorise`, les trois copies du
helper qui les lisait et leurs tests. La règle est dans les profils
ESLint (commit précédent) ; l'exception à un appel est une API nommée,
l'exception à un endroit est de la configuration.

L'audit garde son compte : `tooling.fetch-nu` et `tooling.console`
jouent l'ESLint du dépôt sur ces seules règles, sans information de
type, et se déclarent non exécutables quand la configuration n'active
pas la règle. Le banc les éprouve dans les deux sens, garde comprise.

En déplaçant le compte, une instance voisine : l'audit lisait « 0 »
pour une règle que la version installée ne déclare pas. `eqs inspect`
relève désormais les règles déclarées par chaque contrôle et rend
`null` pour celle que personne n'a jouée.

Cahiers, READMEs, guides, chasse 24 et ETAT disent ce qui est vrai ;
le cahier de @eqs/tooling disait encore « une règle désactivée
localement se fait par commentaire justifié ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#113 demandait que le nom suive l'annuaire et proposait
`updateUserInfoOnLink`. Rejoué sur le rappel OAuth réel (sign-in/social
puis callback/microsoft, locataire simulé) : cette option ne joue qu'au
premier lien, jamais aux connexions suivantes. `overrideUserInfoOnSignIn`
fait les deux. Le rôle reste hors d'atteinte : `mapProfileToUser` ne le
rend plus, et il est `input: false`.

Le même banc a montré pire : better-auth saute un champ `input: false`
même à la création, et `provision: 'jit'` créait chaque compte Entra
sans rôle, quoi que dise `entra.groupes`. Le rôle est mis de côté par
`mapProfileToUser` et posé par le hook de création, seulement pour une
création venue du rappel OAuth.

Les trois tests rougissent quand on retire le correctif ; la proposition
du ticket rougit à la deuxième connexion. D-052 (recommandée : l'adresse
qui suit aussi l'annuaire est à confirmer), chasse 25.

Closes #113

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-platform!114
No description provided.