Deux messages qui envoyaient dans un mur, et une variable que personne ne validait #111

Merged
nicolas-fryder merged 5 commits from nicolas-fryder/dev into main 2026-10-06 08:02:24 +00:00 AGit

#104 — la commande d'isolement n'existait pas

eqs-check imprimait, en cas d'échec, eqs check --only=format. eqs et
eqs-check sont deux binaires, et l'aide de eqs ne mentionne pas check :
la ligne envoyait dans un mur, et le mur laissait croire que la commande avait
disparu du socle. C'est la ligne qu'on lit au moment précis où quelque chose est
cassé.

La chasse a rendu sept instances de plus, dont deux actionnables :

@eqs/cli audit.ts          « .eqs/check.json absent : `eqs check` n'a pas tourné »
@eqs/testing conformite.ts un champ FAIRE : « Isoler la tâche coûteuse
                           (`eqs check --sequential`) »

Un champ faire est littéralement ce qu'il faut faire. Les cinq autres étaient de
la prose, corrigées avec : nommer un binaire qui n'existe pas reste trompeur même
en commentaire.

J'ai écrit dans mon rapport précédent qu'aucun test ne fixait ce message. C'était
vrai du message d'échec, et faux en général : un test pinçait la chaîne du
contrôle testing.check-budget, et il a rougi comme il devait. Le message
d'échec, lui, est fixé maintenant — et le test interdit explicitement eqs check
nu, parce que le tiret est tout le sujet.

#107 — DOCS_ACCESS lue hors schéma

@eqs/platform-nest la lisait sur la source brute. DOCS_ACCESS=publlic
retombait donc en silence sur le défaut — session en production — et la
documentation disparaissait sans un mot.

baseSchema la porte, en enum, sans .default() : la valeur par défaut
dépend de l'environnement et se calcule dans la plateforme. Un défaut ici
écraserait ce calcul.

Ce changement n'est pas additif, contrairement à ce que le ticket annonçait

Mesuré : ecartsEnvExample réclame TOUTE clé du schéma dans .env.example,
optionnelle ou non.

AVEC la ligne → manquantes : aucune
SANS la ligne → manquantes : DOCS_ACCESS

Chaque satellite doit donc ajouter DOCS_ACCESS= à son .env.example, sans quoi
config.env-example rougit. C'est le prix, et il est dans le bon sens : jusqu'ici
un satellite ne pouvait PAS documenter cette variable sans faire échouer sa porte,
puisqu'elle était inconnue du schéma.

Le gabarit et eqs-demo ont reçu la ligne dans la même passe, dans leurs propres
dépôts. Vérifié par la mesure ci-dessus, pas prédit.

Et la porte a exigé trois corrections de plus

EXEMPLE_SOCLE, la fixture de conformité de @eqs/config, et la chaîne pincée de
check-budget. Trois tests rouges avant d'être complétés — exactement le
couplage que le changeset documente, éprouvé sur le socle lui-même avant d'être
annoncé aux satellites.

Ce que je n'ai pas vérifié

L'effet sur un satellite réel : il installe @eqs/config depuis le registre et ne
verra ce changement qu'en 0.15.0.

Et la porte a rougi une première fois sur @eqs/errors, avec deux
emitUnexpectedExit — le flake de fork de la chasse 23, seconde passe verte,
aucun échec nommé.

Closes #104
Closes #107

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

## #104 — la commande d'isolement n'existait pas `eqs-check` imprimait, en cas d'échec, `eqs check --only=format`. `eqs` et `eqs-check` sont **deux binaires**, et l'aide de `eqs` ne mentionne pas `check` : la ligne envoyait dans un mur, et le mur laissait croire que la commande avait disparu du socle. C'est la ligne qu'on lit au moment précis où quelque chose est cassé. La chasse a rendu **sept instances de plus**, dont deux actionnables : @eqs/cli audit.ts « .eqs/check.json absent : `eqs check` n'a pas tourné » @eqs/testing conformite.ts un champ FAIRE : « Isoler la tâche coûteuse (`eqs check --sequential`) » Un champ `faire` est littéralement ce qu'il faut faire. Les cinq autres étaient de la prose, corrigées avec : nommer un binaire qui n'existe pas reste trompeur même en commentaire. J'ai écrit dans mon rapport précédent qu'aucun test ne fixait ce message. C'était vrai du message d'échec, et faux en général : un test pinçait la chaîne du contrôle `testing.check-budget`, et il a rougi comme il devait. Le message d'échec, lui, est fixé maintenant — et le test interdit explicitement `eqs check` nu, parce que le tiret est tout le sujet. ## #107 — `DOCS_ACCESS` lue hors schéma `@eqs/platform-nest` la lisait sur la source brute. `DOCS_ACCESS=publlic` retombait donc en silence sur le défaut — `session` en production — et la documentation disparaissait sans un mot. `baseSchema` la porte, en enum, **sans `.default()`** : la valeur par défaut dépend de l'environnement et se calcule dans la plateforme. Un défaut ici écraserait ce calcul. ### Ce changement n'est pas additif, contrairement à ce que le ticket annonçait Mesuré : `ecartsEnvExample` réclame TOUTE clé du schéma dans `.env.example`, optionnelle ou non. AVEC la ligne → manquantes : aucune SANS la ligne → manquantes : DOCS_ACCESS Chaque satellite doit donc ajouter `DOCS_ACCESS=` à son `.env.example`, sans quoi `config.env-example` rougit. C'est le prix, et il est dans le bon sens : jusqu'ici un satellite ne pouvait PAS documenter cette variable sans faire échouer sa porte, puisqu'elle était inconnue du schéma. Le gabarit et `eqs-demo` ont reçu la ligne dans la même passe, dans leurs propres dépôts. Vérifié par la mesure ci-dessus, pas prédit. ### Et la porte a exigé trois corrections de plus `EXEMPLE_SOCLE`, la fixture de conformité de `@eqs/config`, et la chaîne pincée de `check-budget`. Trois tests rouges avant d'être complétés — exactement le couplage que le changeset documente, éprouvé sur le socle lui-même avant d'être annoncé aux satellites. ## Ce que je n'ai pas vérifié L'effet sur un satellite réel : il installe `@eqs/config` depuis le registre et ne verra ce changement qu'en 0.15.0. Et la porte a rougi une première fois sur `@eqs/errors`, avec deux `emitUnexpectedExit` — le flake de fork de la chasse 23, seconde passe verte, aucun échec nommé. Closes #104 Closes #107 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## #104 — la commande d'isolement n'existait pas

`eqs-check` imprimait, en cas d'échec, `eqs check --only=format`. `eqs` et
`eqs-check` sont **deux binaires**, et l'aide de `eqs` ne mentionne pas `check` :
la ligne envoyait dans un mur, et le mur laissait croire que la commande avait
disparu du socle. C'est la ligne qu'on lit au moment précis où quelque chose est
cassé.

La chasse a rendu **sept instances de plus**, dont deux actionnables :

    @eqs/cli audit.ts          « .eqs/check.json absent : `eqs check` n'a pas tourné »
    @eqs/testing conformite.ts un champ FAIRE : « Isoler la tâche coûteuse
                               (`eqs check --sequential`) »

Un champ `faire` est littéralement ce qu'il faut faire. Les cinq autres étaient de
la prose, corrigées avec : nommer un binaire qui n'existe pas reste trompeur même
en commentaire.

J'ai écrit dans mon rapport précédent qu'aucun test ne fixait ce message. C'était
vrai du message d'échec, et faux en général : un test pinçait la chaîne du
contrôle `testing.check-budget`, et il a rougi comme il devait. Le message
d'échec, lui, est fixé maintenant — et le test interdit explicitement `eqs check`
nu, parce que le tiret est tout le sujet.

## #107 — `DOCS_ACCESS` lue hors schéma

`@eqs/platform-nest` la lisait sur la source brute. `DOCS_ACCESS=publlic`
retombait donc en silence sur le défaut — `session` en production — et la
documentation disparaissait sans un mot.

`baseSchema` la porte, en enum, **sans `.default()`** : la valeur par défaut
dépend de l'environnement et se calcule dans la plateforme. Un défaut ici
écraserait ce calcul.

### Ce changement n'est pas additif, contrairement à ce que le ticket annonçait

Mesuré : `ecartsEnvExample` réclame TOUTE clé du schéma dans `.env.example`,
optionnelle ou non.

    AVEC la ligne → manquantes : aucune
    SANS la ligne → manquantes : DOCS_ACCESS

Chaque satellite doit donc ajouter `DOCS_ACCESS=` à son `.env.example`, sans quoi
`config.env-example` rougit. C'est le prix, et il est dans le bon sens : jusqu'ici
un satellite ne pouvait PAS documenter cette variable sans faire échouer sa porte,
puisqu'elle était inconnue du schéma.

Le gabarit et `eqs-demo` ont reçu la ligne dans la même passe, dans leurs propres
dépôts. Vérifié par la mesure ci-dessus, pas prédit.

### Et la porte a exigé trois corrections de plus

`EXEMPLE_SOCLE`, la fixture de conformité de `@eqs/config`, et la chaîne pincée de
`check-budget`. Trois tests rouges avant d'être complétés — exactement le
couplage que le changeset documente, éprouvé sur le socle lui-même avant d'être
annoncé aux satellites.

## Ce que je n'ai pas vérifié

L'effet sur un satellite réel : il installe `@eqs/config` depuis le registre et ne
verra ce changement qu'en 0.15.0.

Et la porte a rougi une première fois sur `@eqs/errors`, avec deux
`emitUnexpectedExit` — le flake de fork de la chasse 23, seconde passe verte,
aucun échec nommé.

Closes #104
Closes #107

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
J'y écrivais que le gabarit et `eqs-demo` avaient « reçu la ligne dans la même
passe ». J'avais bien édité leurs `.env.example`, et c'était prématuré : leur porte
tourne contre le `@eqs/config` PUBLIÉ — 0.12.1 pour le gabarit — qui ne connaît pas
`DOCS_ACCESS`. Documenter une variable inconnue du schéma fait rougir le contrôle
dans l'autre sens.

Les deux éditions sont annulées. La ligne s'ajoute **à la montée vers 0.15.0**,
pas avant, et le changeset le dit maintenant à la place de l'inverse.

Ce que la mesure a donné, et pourquoi elle ne tranche pas : contre 0.12.1,
`DOCS_ACCESS` ressort bien en « inconnue du schéma » — mais trois
`NEXT_PUBLIC_*` du gabarit en ressortent aussi, alors que sa porte est verte. Le
contrôle fournit donc sa propre liste de variables hors application, que je n'ai
pas reproduite. Je ne sais pas si un `DOCS_ACCESS` prématuré rougirait ou serait
toléré, et je l'écris plutôt que de trancher.

La règle d'ordre ne dépend pas de cette réponse : une ligne qui ne vaut rien avant
la publication et qui peut nuire ne se committe pas avant la publication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`app.use('/api/auth', …)` capte TOUT le sous-arbre en Express, et `toNodeHandler`
rend 404 sur ce que better-auth ne connaît pas **sans appeler `next()`**. Une
route de l'application placée là est enregistrée par Nest, vue par
`DecisionsService`, présente au contrat — et morte à l'exécution (#105).

## Le pire mode d'échec possible

Pas d'erreur au démarrage. Pas de conflit de route signalé. Un 404 à l'exécution,
sur des routes qui ont l'air parfaitement montées. Sur un satellite rétrofitté,
**66 tests d'intégration sur 67** sont tombés d'un coup, et le diagnostic ne
ressemblait pas à sa cause.

`createEqsApp` refuse maintenant de démarrer, nomme les routes, et dit quoi faire.

## Ça lève, ça n'avertit pas

« Un package échoue au démarrage, pas à la première requête. » Le ticket proposait
« refuser ou avertir » ; la doctrine tranche, et ce cas ne laisse aucun doute :
une route inatteignable est une faute de configuration, pas un état dégradé.

Deux détails de placement qui portent tout le correctif :

- le contrôle tourne **après** `listen()` / `init()`. C'est le premier endroit où
  la pile Express porte les routes des contrôleurs ; déclaré plus haut, il ne
  verrait rien et rendrait un vert de complaisance ;
- la couche de better-auth, montée par `app.use`, **n'est pas une `route`** de
  cette pile, donc `routesMontees` ne la voit pas. Ce qui ressort est forcément
  une route de l'application — pas besoin de la filtrer, et c'est écrit dans le
  commentaire pour que personne n'ajoute un filtre qui casserait le sens.

## Qui est concerné, et pourquoi personne ne l'avait vu

Tout dépôt **rétrofitté**, par construction : ses routes d'authentification
maison sont précisément le code que le socle vient remplacer, et elles sont
encore là pendant la transition.

Le gabarit, lui, n'a pas de routes d'authentification propres. Il ne rencontre
jamais ce cas — et c'est exactement pour ça que le défaut a vécu : la forme que le
socle éprouve n'est pas la forme qui casse.

## Deux tests, et le second compte autant

« refuse au démarrage, et nomme la route avec sa méthode » attrape le défaut :
éprouvé en désactivant le garde, il rougit.

« laisse passer une application dont aucune route ne tombe sous ce chemin » est
le miroir, et sans lui un garde qui refuserait TOUT passerait le premier seul.
C'est la forme que j'aurais dû donner d'emblée aux tests de `porter` ce matin.

La phrase manquante est aussi dans la doc de `auth.basePath` : aucune route de
l'application ne peut vivre sous ce chemin. Le ticket la demandait, et c'est ce
qui fait chercher au bon endroit.

## Non fait

La troisième piste du ticket — que `eqs migrate inspect` le signale sur un dépôt
non rétrofitté, AVANT de commencer. Elle est juste et elle appartient à
`@eqs/cli` : il faudrait y reconnaître les contrôleurs sous `auth` sans monter
l'application. À regarder avec #99, qui vient d'apprendre à `profilDe` à lire les
applications d'un workspace.

Closes #105

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`healthChecks` est lu dans les options, donc AVANT que l'application Nest existe.
Une sonde sur un provider — une file BullMQ, un client S3 — n'avait aucun moyen
de l'obtenir (#106).

Pour une base ça ne se voyait pas : le client Prisma vit hors du graphe de
modules, c'est même ce que prescrit `prisma.ts` (#13). Pour le reste, il fallait
une variable mutable posée par `beforeListen` et un `if (!file) return` qui rend
la sonde **silencieusement vacante** si l'ordre changeait. Un satellite l'avait
écrit ; trente allaient le recopier.

`verifier` reçoit maintenant le conteneur.

## `ModuleRef` et non `INestApplication`, contrairement à ce que le ticket demandait

Deux raisons, et la seconde est la vraie :

- **la capacité juste** : une sonde a besoin de RÉSOUDRE un provider, pas de
  piloter le cycle de vie. `ModuleRef.get(X, { strict: false })` est exactement
  le geste du contournement ;
- **l'endroit juste** : `ModuleRef` s'injecte dans `SanteController`, là où les
  sondes tournent. L'`INestApplication` devrait être capturée dans l'assemblage
  puis transmise — c'est-à-dire réintroduire la fermeture que le ticket veut
  supprimer. Le correctif aurait porté le défaut qu'il corrige.

## Rétrocompatible, et un test le fixe

Une fonction qui prend moins d'arguments reste assignable : `verifier: () => …`
compile et tourne comme avant. Le test « pour celle qui ignore l'argument » est à
côté de celui qui résout — sans lui, ce changement casserait tous les satellites
sans que rien ne le dise.

Trois cas : la sonde qui résout, l'ancienne forme, et une sonde dont le provider
est introuvable — 503 et le message, parce qu'une sonde qui n'attrape rien
passerait les deux premiers. Éprouvé en ne passant pas le conteneur : le premier
rougit.

## Une erreur à moi, signalée

Mon premier test attendait `status: 'up'` au niveau global. C'est `'ok'` — seules
les sondes individuelles sont `up`/`down`. Corrigé dans le test, pas dans le code :
c'était mon assertion qui était fausse.

Closes #106

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dans un monorepo, `root` devait à la fois porter les dépendances — d'où le profil
se déduit — et la configuration de test, avec ses `setupFiles`. Les paquets de
plateforme sont dans `apps/api`, `vitest.config.mts` à la racine. Aucune valeur
unique ne satisfaisait les deux (#109).

Et chaque choix refusait **en désignant la mauvaise cause** :

    root = racine     « ni @eqs/platform-nest ni @eqs/platform-next »
    root = apps/api   « aucun fichier de préparation n'installe noSocketInUnit() »

Le second est le pire : le garde EST posé, à la racine, et `eqs doctor` le voit.
Seule la suite ne le trouvait pas, et son message invitait à ajouter ce qui
existait déjà.

## La piste 2, pas la 1

`paquetsEqs` cherche maintenant à la racine ET dans chaque projet du workspace.
`root` vaut la racine du dépôt, les deux usages sont servis par la même valeur,
et il n'y a pas d'option neuve.

Le ticket proposait aussi un `rootPackage` distinct. Ça aurait ajouté de l'API
publique pour contourner **le défaut que #99 vient de corriger dans `@eqs/cli` le
même jour** : le gabarit est mono-application, donc la forme qui casse n'est pas
celle qu'on éprouve. Deux paquets, la même cause, et deux fois la tentation de
déclarer au lieu de chercher.

## La piste 3 coûtait une ligne et valait le temps perdu

    Cherché dans : <racine>/package.json, <racine>/apps/outil/package.json.

L'argument `root` de `deduireProfil` est optionnel, et un test fixe que sans lui
le message reste celui d'avant — sinon ce changement casserait les appelants qui
ne le passent pas.

## Les tests portent sur un workspace

Les deux existants étaient mono-application : la forme qui marchait déjà.
Éprouvé en retirant la recherche dans le workspace — les deux qui comptent
rougissent, les quatre autres non, ce qui est juste.

Closes #109

Co-Authored-By: Claude Opus 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!111
No description provided.