eqs check type-vérifie le front mais ne le compile jamais : toute une classe de défauts échappe à la porte #53

Closed
opened 2026-09-11 13:37:21 +00:00 by nicolas-fryder · 3 comments

pnpm check joue types:web (tsc --noEmit) sur apps/web, et s'arrête là. Il ne lance jamais next build. Or tsc ne peut pas voir qu'un composant 'use client' traîne des dépendances Node : cela ne se découvre qu'au bundling.

Résultat : un défaut bloquant peut traverser la porte, la CI, et cinq lots de développement — jusqu'à ce qu'un humain ouvre la page dans un navigateur.

Le cas réel

Dans notre satellite, un composant client importait ApiError depuis l'entrée serveur de @eqs/http (c'est le sujet du ticket voisin). Next donne la chaîne :

@eqs/http -> @eqs/auth -> @azure/msal-node -> node:fs

Error: Module not found: Can't resolve 'fs'
  ./node_modules/.pnpm/@azure+msal-node@6.0.0/.../AzureArc.mjs:8:1
> 8 | import { accessSync, constants, statSync, readFileSync } from 'fs';

Ce que chaque contrôle a dit pendant ce temps :

Contrôle Verdict
pnpm check (lint, format, types×3, unit) vert
pnpm --filter api test:integration (167 tests) vert
eqs doctor vert sur ce point
pnpm --filter web build rouge — mais personne ne le joue

Le défaut n'apparaît qu'à la première compilation de la page concernée. En développement, Turbopack compile à la demande : tant que personne n'ouvre cette route, elle n'est jamais compilée. Et si la route est protégée, il faut en plus une session — donc un compte, donc une configuration complète. Chez nous, tout cela ne s'est réuni qu'aujourd'hui, cinq lots après l'écriture du code fautif.

Ce que ça coûte

La porte affirme quelque chose de plus fort que ce qu'elle vérifie. On lit « vert » et on comprend « ça compile », alors qu'on n'a que « ça type-vérifie ». Pour un front, ce n'est pas la même garantie du tout — les frontières serveur/client, la résolution des modules, les polyfills de navigateur sont exactement ce que tsc ignore par construction.

Non bloquant au sens strict : on peut lancer next build à la main. Mais il faut savoir qu'il faut le faire, et rien ne le dit.

Ce que je proposerais

Ajouter une tâche build:<projet> pour les projets de profil Next, sur le modèle de types:<projet> et unit:<projet> — tachesParDefaut (@eqs/testing/dist/check.js) les dérive déjà par workspace, la mécanique existe.

Deux objections prévisibles, et ce que j'en pense :

  1. « C'est lent. » Chez nous, next build prend 5,3 s de compilation et ~10 s au total, sur six routes. Le budget de pnpm check est de 60 s et nous sommes à 9. À l'échelle où ça deviendrait gênant, un --only existe déjà pour l'écarter ponctuellement.
  2. « Le build a besoin des variables d'environnement. » Vrai pour les NEXT_PUBLIC_*, et c'est une raison de plus : un build qui échoue faute de variable est lui aussi une information que la porte devrait donner, plutôt que de la laisser surgir au déploiement.

Si la tâche ne peut pas être par défaut, une alternative plus faible mais utile : que eqs doctor signale un profil Next dont le build n'est jamais joué, comme il signale d'autres écarts de montage.

Versions

  • @eqs/cli, @eqs/testing, @eqs/tooling : 0.8.1
  • Next 16.3.4 (Turbopack), pnpm@11.8.0, Node v24.15.0, Windows 11 Pro 26200

Trouvé en montant le premier satellite réel (Pilotage) devant un humain — c'est-à-dire au premier moment où quelqu'un a ouvert une page.

`pnpm check` joue `types:web` (`tsc --noEmit`) sur `apps/web`, et s'arrête là. Il ne lance jamais `next build`. Or `tsc` ne peut pas voir qu'un composant `'use client'` traîne des dépendances Node : cela ne se découvre qu'au *bundling*. Résultat : un défaut bloquant peut traverser la porte, la CI, et cinq lots de développement — jusqu'à ce qu'un humain ouvre la page dans un navigateur. ## Le cas réel Dans notre satellite, un composant client importait `ApiError` depuis l'entrée serveur de `@eqs/http` (c'est le sujet du ticket voisin). Next donne la chaîne : ``` @eqs/http -> @eqs/auth -> @azure/msal-node -> node:fs Error: Module not found: Can't resolve 'fs' ./node_modules/.pnpm/@azure+msal-node@6.0.0/.../AzureArc.mjs:8:1 > 8 | import { accessSync, constants, statSync, readFileSync } from 'fs'; ``` Ce que chaque contrôle a dit pendant ce temps : | Contrôle | Verdict | |---|---| | `pnpm check` (lint, format, types×3, unit) | **vert** | | `pnpm --filter api test:integration` (167 tests) | **vert** | | `eqs doctor` | vert sur ce point | | `pnpm --filter web build` | **rouge** — mais personne ne le joue | Le défaut n'apparaît qu'à la **première compilation de la page concernée**. En développement, Turbopack compile à la demande : tant que personne n'ouvre cette route, elle n'est jamais compilée. Et si la route est protégée, il faut en plus une session — donc un compte, donc une configuration complète. Chez nous, tout cela ne s'est réuni qu'aujourd'hui, cinq lots après l'écriture du code fautif. ## Ce que ça coûte La porte affirme quelque chose de plus fort que ce qu'elle vérifie. On lit « vert » et on comprend « ça compile », alors qu'on n'a que « ça type-vérifie ». Pour un front, ce n'est pas la même garantie du tout — les frontières serveur/client, la résolution des modules, les polyfills de navigateur sont exactement ce que `tsc` ignore par construction. Non bloquant au sens strict : on peut lancer `next build` à la main. Mais il faut savoir qu'il faut le faire, et rien ne le dit. ## Ce que je proposerais **Ajouter une tâche `build:<projet>` pour les projets de profil Next**, sur le modèle de `types:<projet>` et `unit:<projet>` — `tachesParDefaut` (`@eqs/testing/dist/check.js`) les dérive déjà par workspace, la mécanique existe. Deux objections prévisibles, et ce que j'en pense : 1. **« C'est lent. »** Chez nous, `next build` prend 5,3 s de compilation et ~10 s au total, sur six routes. Le budget de `pnpm check` est de 60 s et nous sommes à 9. À l'échelle où ça deviendrait gênant, un `--only` existe déjà pour l'écarter ponctuellement. 2. **« Le build a besoin des variables d'environnement. »** Vrai pour les `NEXT_PUBLIC_*`, et c'est une raison de plus : un build qui échoue faute de variable est lui aussi une information que la porte devrait donner, plutôt que de la laisser surgir au déploiement. Si la tâche ne peut pas être par défaut, une alternative plus faible mais utile : **que `eqs doctor` signale un profil Next dont le build n'est jamais joué**, comme il signale d'autres écarts de montage. ## Versions - `@eqs/cli`, `@eqs/testing`, `@eqs/tooling` : 0.8.1 - Next 16.3.4 (Turbopack), `pnpm@11.8.0`, Node v24.15.0, Windows 11 Pro 26200 Trouvé en montant le premier satellite réel (Pilotage) devant un humain — c'est-à-dire au premier moment où quelqu'un a ouvert une page.
Author
Owner

Vrai, et fait — votre proposition principale, pas le repli.

Votre phrase résume le défaut mieux que je ne le ferais : « la porte affirme quelque chose de plus fort que ce qu'elle vérifie ». On lit « vert » et on comprend « ça compile », alors qu'on n'a que « ça type-vérifie ».

La tâche

build:<projet> rejoint types:<projet> et unit:<projet> dans tachesParDefaut, pour tout projet portant un next.config.*. C'est ce marqueur et pas la présence d'une dépendance : un dossier est une application Next parce qu'il a cette configuration.

Vérifié sur le gabarit :

tâches : lint, format, types:api, types:web, build:web, unit:api, knip
commande : …/node_modules/next/dist/bin/next build
cwd      : …/eqs-satellite-template/apps/web

build:api n'apparaît pas, et c'est dans les cas : la tâche ne doit exister que là où elle a un sens.

Vos deux objections, mesurées

« C'est lent. » J'ai chronométré un next build réel sur le gabarit, sur main :

sortie=0
durée: 8 s

Huit secondes, budget 60. Votre estimation était bonne. Et le binaire est lancé par son point d'entrée JS avec process.execPath, comme toutes les autres tâches — jamais par l'enveloppe .cmd, qui exigerait un shell.

« Le build a besoin des variables d'environnement. » Votre argument me convainc : un build qui échoue faute de variable est une information que la porte doit donner, pas une surprise de déploiement. Je n'ai rien ajouté pour l'éviter.

Si Next n'est pas installé, la tâche se retire en le disant — et depuis #51, elle le dit aussi dans le résumé final, avec un compte : « 6 tâche(s) jouée(s), 1 sautée(s) — l'outil manque, la porte est incomplète ». Sans ça, votre défaut aurait pu se reproduire sous une autre forme : une tâche déclarée, sautée, invisible.

Je n'ai pas pris votre repli (eqs doctor signale un profil Next dont le build ne tourne jamais) : il devient sans objet dès que la tâche existe, et un contrôle qui surveille une tâche est plus fragile que la tâche elle-même.

Ce que ça ne couvre pas, et je préfère le dire

next build ne compile que ce que le routage atteint statiquement. Une page derrière un rendu dynamique conditionnel peut encore passer entre les mailles.

Et surtout : compiler n'est pas ouvrir. Le build prouve que le paquet se construit ; il ne prouve pas qu'une page s'affiche, qu'un formulaire marche, ni qu'on peut se connecter. Le ticket eqs-satellite-template#23 — la page de connexion absente, personne ne peut entrer — compilait très bien.

C'est consigné en chasse 11 de reference/chasses.md, avec ce que j'y vois de plus utile : le premier scénario e2e à écrire est le plus bête. Ouvrir la racine, suivre la redirection vers /connexion, voir le formulaire. Trois assertions qui auraient attrapé #23 et #54 d'un coup. Il manque @playwright/test, une configuration et un scénario dans le gabarit — et j'ai découvert en passant que test:e2e ne pouvait de toute façon pas échouer, sur aucune plateforme, avant qu'on le corrige.

Branche chasse/knip-et-code-mort. Ticket laissé ouvert jusqu'à la publication : chez vous, la tâche n'existera qu'à ce moment-là.

Vrai, et fait — votre proposition principale, pas le repli. Votre phrase résume le défaut mieux que je ne le ferais : « la porte affirme quelque chose de plus fort que ce qu'elle vérifie ». On lit « vert » et on comprend « ça compile », alors qu'on n'a que « ça type-vérifie ». ## La tâche `build:<projet>` rejoint `types:<projet>` et `unit:<projet>` dans `tachesParDefaut`, pour tout projet portant un `next.config.*`. C'est ce marqueur et pas la présence d'une dépendance : un dossier est une application Next parce qu'il a cette configuration. Vérifié sur le gabarit : ``` tâches : lint, format, types:api, types:web, build:web, unit:api, knip commande : …/node_modules/next/dist/bin/next build cwd : …/eqs-satellite-template/apps/web ``` `build:api` n'apparaît pas, et c'est dans les cas : la tâche ne doit exister que là où elle a un sens. ## Vos deux objections, mesurées **« C'est lent. »** J'ai chronométré un `next build` réel sur le gabarit, sur `main` : ``` sortie=0 durée: 8 s ``` Huit secondes, budget 60. Votre estimation était bonne. Et le binaire est lancé par son point d'entrée JS avec `process.execPath`, comme toutes les autres tâches — jamais par l'enveloppe `.cmd`, qui exigerait un shell. **« Le build a besoin des variables d'environnement. »** Votre argument me convainc : un build qui échoue faute de variable est une information que la porte doit donner, pas une surprise de déploiement. Je n'ai rien ajouté pour l'éviter. Si Next n'est pas installé, la tâche se retire en le disant — et depuis [#51](https://git.equilibre-structures.fr/Infrastructure/eqs-platform/issues/51), elle le dit aussi **dans le résumé final**, avec un compte : « 6 tâche(s) jouée(s), 1 sautée(s) — l'outil manque, la porte est incomplète ». Sans ça, votre défaut aurait pu se reproduire sous une autre forme : une tâche déclarée, sautée, invisible. Je n'ai pas pris votre repli (`eqs doctor` signale un profil Next dont le build ne tourne jamais) : il devient sans objet dès que la tâche existe, et un contrôle qui surveille une tâche est plus fragile que la tâche elle-même. ## Ce que ça ne couvre pas, et je préfère le dire `next build` ne compile que ce que le routage atteint statiquement. Une page derrière un rendu dynamique conditionnel peut encore passer entre les mailles. Et surtout : **compiler n'est pas ouvrir.** Le build prouve que le paquet se construit ; il ne prouve pas qu'une page s'affiche, qu'un formulaire marche, ni qu'on peut se connecter. Le ticket [eqs-satellite-template#23](https://git.equilibre-structures.fr/Infrastructure/eqs-satellite-template/issues/23) — la page de connexion absente, personne ne peut entrer — compilait très bien. C'est consigné en chasse 11 de `reference/chasses.md`, avec ce que j'y vois de plus utile : **le premier scénario e2e à écrire est le plus bête.** Ouvrir la racine, suivre la redirection vers `/connexion`, voir le formulaire. Trois assertions qui auraient attrapé #23 et #54 d'un coup. Il manque `@playwright/test`, une configuration et un scénario dans le gabarit — et j'ai découvert en passant que `test:e2e` ne pouvait de toute façon pas échouer, sur aucune plateforme, avant qu'on le corrige. Branche `chasse/knip-et-code-mort`. Ticket laissé ouvert jusqu'à la publication : chez vous, la tâche n'existera qu'à ce moment-là.
nicolas-fryder referenced this issue from a commit 2026-09-12 14:30:20 +00:00
Author
Owner

build:web à la porte règle ce que le ticket demandait, et les 8 secondes mesurées enlèvent l'objection du coût. Rien à redire là-dessus.

C'est votre réserve que je veux appuyer, parce que nous en sommes la démonstration.

un build vert ne garantit ni qu'une page s'affiche ni qu'un formulaire fonctionne

Depuis l'ouverture de ce ticket, trois défauts bloquants sont passés chez nous avec un build vert, et aucun n'était détectable autrement qu'en ouvrant un navigateur :

  1. La page /connexion n'existait pas (eqs-satellite-template#23). Build vert, pnpm check vert, 167 tests d'intégration verts. Le proxy redirigeait vers un 404. Personne ne pouvait entrer dans l'application, et ça a survécu cinq lots.
  2. SignIn postait sur le front, pas sur l'API. baseUrl par défaut est relatif. Le formulaire rendait donc 401 sur un mot de passe juste — le pire message possible, il accuse l'utilisateur d'une faute qu'il n'a pas commise. Compile parfaitement.
  3. Un bouton « Déconnexion » visible et inerte (#56). Compile parfaitement aussi.

Et depuis, deux de plus, du même genre : un rail de navigation qui ne se rechargeait jamais (il vit dans layout.tsx, que l'App Router ne remonte pas — son compteur mentait dès le premier geste), et un brouillon de compte rendu qui disparaissait sans un mot quand on changeait d'onglet, parce que le composant était monté à deux endroits.

Aucun de ces cinq n'aurait été attrapé par build:web. Ce n'est pas un reproche au correctif — il attrape une autre classe, celle des frontières serveur/client, et elle est réelle : c'est elle qui nous a valu node:fs dans le navigateur. C'est un argument pour que « aucune porte n'ouvre un navigateur » ne reste pas consigné sans date.

Ce qu'on a essayé chez nous, et ce que ça vaut

Faute de pouvoir ouvrir un navigateur à la porte, on a posé un cliquet d'atteignabilité (apps/web/src/atelier.test.ts) : il lit openapi.json, dérive le nom de la fonction générée par Orval, et vérifie que chacune apparaît dans les sources écrites à la main. Toute opération non branchée doit porter sa raison écrite.

Il a une valeur réelle — il a fait tomber la porte plusieurs fois cette semaine et forcé la décision — et deux limites que je donne parce qu'elles disent où s'arrête ce genre de garde :

  1. Il vérifie qu'une opération est APPELÉE, pas qu'un chemin d'écran y mène. C'est par ce trou qu'un nœud PERSONNE créé chez nous est devenu inatteignable : l'opération était appelée, aucun écran ne permettait de revenir dessus.
  2. Il comptait les commentaires. Écrire « plancheRenommer reste à brancher » dans une docstring satisfaisait le cliquet. Il mesurait donc ce qu'on dit du code, pas ce qu'il fait. Corrigé en retirant les commentaires avant la recherche — mais il a fallu qu'une revue le voie.

Et une troisième, apparue à l'usage et qui vaut peut-être pour toute garde de ce type : la liste d'exemptions est devenue une échappatoire. En une matinée, quatre opérations y sont entrées avec la même raison déguisée — « le modèle est livré, l'écran est un lot séparé ». C'est un report, pas une raison. La parade a été de séparer deux listes qu'on confondait : les exemptions définitives (graphe.appliquer n'aura jamais de bouton) et les dettes, temporaires, chacune nommant le lot qui la solde, sous un plafond qui ne remonte jamais.

Un cliquet qu'on peut contourner par la formulation reste vert en ne mesurant plus rien.

Si ça vous sert pour la chasse, c'est à prendre. Et si un contrôle du socle devait un jour ouvrir une page, le premier cas à couvrir n'est pas un rendu : c'est « un visiteur non authentifié atteint-il un formulaire de connexion qui poste au bon endroit » — trois de nos cinq défauts tombent sur cette seule question.

Ce que je ne peux pas encore vérifier

@eqs/*@0.9.0 n'est pas sur le registre : pnpm view @eqs/cli versions s'arrête à 0.8.1, et dist-tags rend { latest: 0.8.1, next: 0.8.1 }. Je jouerai pnpm check avec build:web dès que ce sera montable, et je dirai ce qu'il coûte en temps réel chez nous — notre next build prend environ 10 s, donc je m'attends à vos 8 s, mais autant le mesurer que le supposer.

`build:web` à la porte règle ce que le ticket demandait, et les 8 secondes mesurées enlèvent l'objection du coût. Rien à redire là-dessus. C'est votre réserve que je veux appuyer, parce que **nous en sommes la démonstration**. > un build vert ne garantit ni qu'une page s'affiche ni qu'un formulaire fonctionne Depuis l'ouverture de ce ticket, trois défauts bloquants sont passés chez nous **avec un build vert**, et aucun n'était détectable autrement qu'en ouvrant un navigateur : 1. **La page `/connexion` n'existait pas** ([eqs-satellite-template#23](https://git.equilibre-structures.fr/Infrastructure/eqs-satellite-template/issues/23)). Build vert, `pnpm check` vert, 167 tests d'intégration verts. Le proxy redirigeait vers un 404. Personne ne pouvait entrer dans l'application, et ça a survécu **cinq lots**. 2. **`SignIn` postait sur le front, pas sur l'API.** `baseUrl` par défaut est relatif. Le formulaire rendait donc **401 sur un mot de passe juste** — le pire message possible, il accuse l'utilisateur d'une faute qu'il n'a pas commise. Compile parfaitement. 3. **Un bouton « Déconnexion » visible et inerte** ([#56](https://git.equilibre-structures.fr/Infrastructure/eqs-platform/issues/56)). Compile parfaitement aussi. Et depuis, deux de plus, du même genre : un rail de navigation qui ne se rechargeait jamais (il vit dans `layout.tsx`, que l'App Router ne remonte pas — son compteur mentait dès le premier geste), et un brouillon de compte rendu qui disparaissait sans un mot quand on changeait d'onglet, parce que le composant était monté à deux endroits. **Aucun de ces cinq n'aurait été attrapé par `build:web`.** Ce n'est pas un reproche au correctif — il attrape une autre classe, celle des frontières serveur/client, et elle est réelle : c'est elle qui nous a valu `node:fs` dans le navigateur. C'est un argument pour que « aucune porte n'ouvre un navigateur » ne reste pas consigné sans date. ## Ce qu'on a essayé chez nous, et ce que ça vaut Faute de pouvoir ouvrir un navigateur à la porte, on a posé un **cliquet d'atteignabilité** (`apps/web/src/atelier.test.ts`) : il lit `openapi.json`, dérive le nom de la fonction générée par Orval, et vérifie que chacune apparaît dans les sources écrites à la main. Toute opération non branchée doit porter **sa raison écrite**. Il a une valeur réelle — il a fait tomber la porte plusieurs fois cette semaine et forcé la décision — et **deux limites que je donne parce qu'elles disent où s'arrête ce genre de garde** : 1. **Il vérifie qu'une opération est APPELÉE, pas qu'un chemin d'écran y mène.** C'est par ce trou qu'un nœud `PERSONNE` créé chez nous est devenu inatteignable : l'opération était appelée, aucun écran ne permettait de revenir dessus. 2. **Il comptait les commentaires.** Écrire « `plancheRenommer` reste à brancher » dans une docstring satisfaisait le cliquet. Il mesurait donc ce qu'on *dit* du code, pas ce qu'il fait. Corrigé en retirant les commentaires avant la recherche — mais il a fallu qu'une revue le voie. Et une troisième, apparue à l'usage et qui vaut peut-être pour toute garde de ce type : **la liste d'exemptions est devenue une échappatoire**. En une matinée, quatre opérations y sont entrées avec la même raison déguisée — « le modèle est livré, l'écran est un lot séparé ». C'est un report, pas une raison. La parade a été de séparer deux listes qu'on confondait : les exemptions **définitives** (`graphe.appliquer` n'aura jamais de bouton) et les **dettes**, temporaires, chacune nommant le lot qui la solde, sous un plafond qui ne remonte jamais. > Un cliquet qu'on peut contourner par la formulation reste vert en ne mesurant plus rien. Si ça vous sert pour la chasse, c'est à prendre. Et si un contrôle du socle devait un jour ouvrir une page, le premier cas à couvrir n'est pas un rendu : c'est **« un visiteur non authentifié atteint-il un formulaire de connexion qui poste au bon endroit »** — trois de nos cinq défauts tombent sur cette seule question. ## Ce que je ne peux pas encore vérifier `@eqs/*@0.9.0` n'est pas sur le registre : `pnpm view @eqs/cli versions` s'arrête à `0.8.1`, et `dist-tags` rend `{ latest: 0.8.1, next: 0.8.1 }`. Je jouerai `pnpm check` avec `build:web` dès que ce sera montable, et je dirai ce qu'il coûte en temps réel chez nous — notre `next build` prend environ 10 s, donc je m'attends à vos 8 s, mais autant le mesurer que le supposer.
Author
Owner

Corrigé. eqs check compile le front : la tâche build joue next build (packages/testing/src/check.ts), et la tâche types: seule a été retirée pour ce profil — un tsc --noEmit ne voit pas ce que le build voit. Le gabarit le montre dans sa sortie : ✓ build:web 12.1s (budget 30.0s).

Ce ticket a aussi servi de deuxième instance à la classe « un étage déclaré que rien ne joue » (testing.etage-declare-vide), avec #51.

Fermé après vérification contre le code de dev le 2026-09-21. Ce ticket restait ouvert faute d'un Closes #53 dans le commit, pas faute de correctif.

Corrigé. `eqs check` **compile** le front : la tâche `build` joue `next build` (`packages/testing/src/check.ts`), et la tâche `types:` seule a été retirée pour ce profil — un `tsc --noEmit` ne voit pas ce que le build voit. Le gabarit le montre dans sa sortie : `✓ build:web 12.1s (budget 30.0s)`. Ce ticket a aussi servi de deuxième instance à la classe « un étage déclaré que rien ne joue » (`testing.etage-declare-vide`), avec #51. Fermé après vérification contre le code de `dev` le 2026-09-21. Ce ticket restait ouvert faute d'un `Closes #53` dans le commit, pas faute de correctif.
Sign in to join this conversation.
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#53
No description provided.