runConformance : root sert deux choses qui ne vivent pas au même endroit dans un monorepo #109

Closed
opened 2026-10-01 13:57:07 +00:00 by nicolas-fryder · 2 comments

Ce qui se passe

root est employé pour deux usages qui, dans un monorepo, pointent sur deux répertoires différents :

  1. Déduire le profil — deduireProfil lit les dépendances du package.json de root pour y trouver @eqs/platform-nest ou @eqs/platform-next. Dans nuage, c'est apps/api/package.json.
  2. Les contrôles du dispositif de test — ils cherchent vitest.config.mts et ses setupFiles sous root. Dans nuage, c'est la racine du dépôt.

Un seul root ne peut pas satisfaire les deux, et chaque choix produit un refus qui désigne la mauvaise cause :

# root = racine du dépôt
Error: runConformance : ni @eqs/platform-nest ni @eqs/platform-next dans les
dépendances. Passez `profile` si le dépôt ne suit aucun des deux profils.

# root = apps/api (le défaut, process.cwd() sous `pnpm --filter api`)
[bloquant] testing.unit-sans-io
  aucun fichier de préparation n'installe le garde `noSocketInUnit()`
  → Ajouter `setupFiles` à la configuration de test et y appeler noSocketInUnit()

Le second est le plus trompeur : le garde est posé, à la racine, et eqs doctor le voit parfaitement. Seule la suite de conformité ne le trouve pas, et son message invite à ajouter ce qui existe déjà.

Le gabarit ne le rencontre pas : son vitest.config.mts et son apps/api sont dans le même dépôt, mais le profil se déduit depuis la racine parce que c'est là que ses @eqs/* sont installés. Dans un monorepo pnpm où l'application a ses propres dépendances, les deux divergent.

Ce que nuage a fait

await runConformance(() => createEqsApp({ ... }), {
  root: racineDepot(__dirname),  // la racine : vitest.config.mts y vit
  profile: 'nest',               // nommé, puisque la déduction ne peut plus marcher
  options: { nonExecutables: 'avertir' },
})

Ça marche, et c'est écrit. Mais il a fallu deux échecs pour le trouver, et aucun des deux messages n'y menait.

Pistes

  1. Séparer les deux. Un rootPackage (où chercher les dépendances) distinct de root (où chercher la configuration de test), le second valant le premier par défaut. C'est additif.
  2. Ou chercher plus largement : si root/package.json ne déclare aucun paquet de plateforme, regarder les workspaces / pnpm-workspace.yaml avant d'abandonner. Le monorepo est la forme normale d'un satellite avec un front.
  3. Au minimum, enrichir le message de deduireProfil : « cherché dans <chemin>/package.json » rendrait le diagnostic immédiat.

La troisième coûte une ligne et aurait suffi à me faire gagner le temps perdu.

Trouvé en rétrofittant nuage, socle 0.14.0, @eqs/testing@0.14.0.

## Ce qui se passe `root` est employé pour deux usages qui, dans un monorepo, pointent sur deux répertoires différents : 1. **Déduire le profil** — `deduireProfil` lit les dépendances du `package.json` de `root` pour y trouver `@eqs/platform-nest` ou `@eqs/platform-next`. Dans nuage, c'est `apps/api/package.json`. 2. **Les contrôles du dispositif de test** — ils cherchent `vitest.config.mts` et ses `setupFiles` sous `root`. Dans nuage, c'est la **racine du dépôt**. Un seul `root` ne peut pas satisfaire les deux, et chaque choix produit un refus qui désigne la mauvaise cause : ```console # root = racine du dépôt Error: runConformance : ni @eqs/platform-nest ni @eqs/platform-next dans les dépendances. Passez `profile` si le dépôt ne suit aucun des deux profils. # root = apps/api (le défaut, process.cwd() sous `pnpm --filter api`) [bloquant] testing.unit-sans-io aucun fichier de préparation n'installe le garde `noSocketInUnit()` → Ajouter `setupFiles` à la configuration de test et y appeler noSocketInUnit() ``` Le second est le plus trompeur : le garde **est** posé, à la racine, et `eqs doctor` le voit parfaitement. Seule la suite de conformité ne le trouve pas, et son message invite à ajouter ce qui existe déjà. Le gabarit ne le rencontre pas : son `vitest.config.mts` et son `apps/api` sont dans le même dépôt, mais le profil se déduit depuis la racine parce que c'est là que ses `@eqs/*` sont installés. Dans un monorepo pnpm où l'application a ses propres dépendances, les deux divergent. ## Ce que nuage a fait ```ts await runConformance(() => createEqsApp({ ... }), { root: racineDepot(__dirname), // la racine : vitest.config.mts y vit profile: 'nest', // nommé, puisque la déduction ne peut plus marcher options: { nonExecutables: 'avertir' }, }) ``` Ça marche, et c'est écrit. Mais il a fallu deux échecs pour le trouver, et aucun des deux messages n'y menait. ## Pistes 1. **Séparer les deux.** Un `rootPackage` (où chercher les dépendances) distinct de `root` (où chercher la configuration de test), le second valant le premier par défaut. C'est additif. 2. **Ou chercher plus largement** : si `root/package.json` ne déclare aucun paquet de plateforme, regarder les `workspaces` / `pnpm-workspace.yaml` avant d'abandonner. Le monorepo est la forme normale d'un satellite avec un front. 3. **Au minimum**, enrichir le message de `deduireProfil` : « cherché dans `<chemin>/package.json` » rendrait le diagnostic immédiat. La troisième coûte une ligne et aurait suffi à me faire gagner le temps perdu. Trouvé en rétrofittant nuage, socle 0.14.0, `@eqs/testing@0.14.0`.
Author
Owner

Même cause, troisième endroit — et celui-ci est le plus dangereux des trois, parce qu'il ne produit pas une erreur mais un zéro faussement rassurant.

http.fetch-brut scanne join(root, 'src'). Dans un monorepo, <racine>/src n'existe pas : le code est sous apps/api/src et apps/front/src. La règle ne trouve donc rien, et eqs inspect écrit :

outgoing.rawHttpCalls : null → 0

Zéro. Alors que nuage a quatre appels fetch directs. Mesuré en appelant la règle à la main avec chaque racine candidate :

.            → 0 constat(s)
apps/api     → 3 constat(s)
      src/common/upstream.ts:57
      src/lidar-hd/lidar-hd-download.processor.ts:73
      src/lidar-hd/lidar-hd-download.processor.ts:77
apps/front   → 1 constat(s)
      src/lib/api.ts:16

Ce qui en fait un cas à part des deux autres : un root qui empêche deduireProfil lève, et le faux unit-sans-io refuse. Celui-ci passe, et il écrit dans la baseline un chiffre qui dit « aucun appel brut » à un dépôt qui en a quatre. C'est précisément le genre de vert qui fait cesser de regarder — le même motif que le ✓ en 0 ms du ticket #108.

Le champ est aussi l'un de ceux qui mesurent l'invariant 4 (« tout appel interne passe par un client généré »). Un satellite monorepo le verra toujours satisfait.

La piste 1 du ticket (séparer root de rootPackage) ne suffirait pas ici : il faudrait que la règle sache qu'il y a plusieurs arbres de sources. Deux formes possibles :

  • accepter roots: string[] (ou sources: string[]) en plus de root, et scanner chacun ;
  • à défaut de <root>/src, lire les packages du pnpm-workspace.yaml et scanner le src de chacun — ce qui rendrait le monorepo correct sans que le satellite ait à configurer quoi que ce soit.

La seconde a ma préférence : elle fait que le cas marche par défaut, et le défaut est la seule configuration que personne n'oublie.

En attendant, nuage ne peut pas se fier à ce champ, et ses quatre appels bruts sont connus et suivis de son côté.

Même cause, troisième endroit — et celui-ci est le plus dangereux des trois, parce qu'il ne produit pas une erreur mais un **zéro faussement rassurant**. `http.fetch-brut` scanne `join(root, 'src')`. Dans un monorepo, `<racine>/src` n'existe pas : le code est sous `apps/api/src` et `apps/front/src`. La règle ne trouve donc rien, et `eqs inspect` écrit : ``` outgoing.rawHttpCalls : null → 0 ``` Zéro. Alors que nuage a **quatre** appels `fetch` directs. Mesuré en appelant la règle à la main avec chaque racine candidate : ``` . → 0 constat(s) apps/api → 3 constat(s) src/common/upstream.ts:57 src/lidar-hd/lidar-hd-download.processor.ts:73 src/lidar-hd/lidar-hd-download.processor.ts:77 apps/front → 1 constat(s) src/lib/api.ts:16 ``` Ce qui en fait un cas à part des deux autres : un `root` qui empêche `deduireProfil` lève, et le faux `unit-sans-io` refuse. Celui-ci **passe**, et il écrit dans la baseline un chiffre qui dit « aucun appel brut » à un dépôt qui en a quatre. C'est précisément le genre de vert qui fait cesser de regarder — le même motif que le ✓ en 0 ms du ticket #108. Le champ est aussi l'un de ceux qui mesurent l'invariant 4 (« tout appel interne passe par un client généré »). Un satellite monorepo le verra toujours satisfait. La piste 1 du ticket (séparer `root` de `rootPackage`) ne suffirait pas ici : il faudrait que la règle sache qu'il y a **plusieurs** arbres de sources. Deux formes possibles : - accepter `roots: string[]` (ou `sources: string[]`) en plus de `root`, et scanner chacun ; - à défaut de `<root>/src`, lire les `packages` du `pnpm-workspace.yaml` et scanner le `src` de chacun — ce qui rendrait le monorepo correct sans que le satellite ait à configurer quoi que ce soit. La seconde a ma préférence : elle fait que le cas marche par défaut, et le défaut est la seule configuration que personne n'oublie. En attendant, nuage ne peut pas se fier à ce champ, et ses quatre appels bruts sont connus et suivis de son côté.
Author
Owner

Corrigé par 9a690af, PR #111, changeset paquets-eqs-du-workspace. Pistes 2 et 3 faites, piste 1 écartée avec sa raison. Fermeture à la fusion.

La piste 2, et pourquoi 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.

Votre piste 1 — un rootPackage distinct — aurait ajouté de l'API publique pour contourner exactement le défaut que #99 vient de corriger dans @eqs/cli, le même jour : profilDe y lisait aussi le seul manifeste de la racine, et tout dépôt nest rendait profile: null. Deux paquets, la même cause, et deux fois la même tentation — déclarer au lieu de chercher.

Votre propre phrase le disait : « le monorepo est la forme normale d'un satellite avec un front ». Si c'est la forme normale, la déduction doit la connaître, pas demander qu'on la lui épelle.

La piste 3 coûtait une ligne, et vous aviez raison sur sa valeur

runConformance : ni @eqs/platform-nest ni @eqs/platform-next dans les dépendances.
Cherché dans : <racine>/package.json, <racine>/apps/outil/package.json.
Passez `profile` si le dépôt ne suit aucun des deux profils.

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

Le second message était le vrai piège

le garde est posé, à la racine, et eqs doctor le voit parfaitement. Seule la suite de conformité ne le trouve pas, et son message invite à ajouter ce qui existe déjà.

C'est la famille qui a dominé la journée : un dispositif qui se prononce sur ce qu'il ne regarde pas. #100 lintait 12 fichiers sur 31 en rendant zéro violation, #108 affichait ✓ sur deux paquets qui n'avaient évalué aucune règle, #106 avait une sonde que if (!file) return rendait vacante. Ici, un message qui demande d'ajouter l'existant.

Ce que votre contournement devient

await runConformance(() => createEqsApp({ ... }), {
  root: racineDepot(__dirname),
  options: { nonExecutables: 'avertir' },
})

profile: 'nest' n'est plus nécessaire — la déduction trouve @eqs/platform-nest dans apps/api. Le laisser ne gêne pas : une déclaration continue de l'emporter sur la déduction.

Les quatre tests neufs portent sur un workspace, parce que les deux existants étaient mono-application. Éprouvés en retirant la recherche dans le workspace : les deux qui comptent rougissent, les autres non.

Huit changesets en attente. 0.15.0 quand le flux se tarira.

Corrigé par `9a690af`, PR #111, changeset `paquets-eqs-du-workspace`. Pistes 2 et 3 faites, piste 1 écartée avec sa raison. Fermeture à la fusion. ## La piste 2, et pourquoi 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**. Votre piste 1 — un `rootPackage` distinct — aurait ajouté de l'API publique pour contourner exactement le défaut que #99 vient de corriger dans `@eqs/cli`, le même jour : `profilDe` y lisait aussi le seul manifeste de la racine, et tout dépôt `nest` rendait `profile: null`. Deux paquets, la même cause, et deux fois la même tentation — déclarer au lieu de chercher. Votre propre phrase le disait : « le monorepo est la forme normale d'un satellite avec un front ». Si c'est la forme normale, la déduction doit la connaître, pas demander qu'on la lui épelle. ## La piste 3 coûtait une ligne, et vous aviez raison sur sa valeur ``` runConformance : ni @eqs/platform-nest ni @eqs/platform-next dans les dépendances. Cherché dans : <racine>/package.json, <racine>/apps/outil/package.json. Passez `profile` si le dépôt ne suit aucun des deux profils. ``` L'argument `root` de `deduireProfil` est **optionnel**, et un test fixe que sans lui le message reste celui d'avant — sinon le changement casserait les appelants qui ne le passent pas. ## Le second message était le vrai piège > le garde **est** posé, à la racine, et `eqs doctor` le voit parfaitement. Seule la suite de conformité ne le trouve pas, et son message invite à ajouter ce qui existe déjà. C'est la famille qui a dominé la journée : un dispositif qui se prononce sur ce qu'il ne regarde pas. #100 lintait 12 fichiers sur 31 en rendant zéro violation, #108 affichait ✓ sur deux paquets qui n'avaient évalué aucune règle, #106 avait une sonde que `if (!file) return` rendait vacante. Ici, un message qui demande d'ajouter l'existant. ## Ce que votre contournement devient ```ts await runConformance(() => createEqsApp({ ... }), { root: racineDepot(__dirname), options: { nonExecutables: 'avertir' }, }) ``` `profile: 'nest'` n'est plus nécessaire — la déduction trouve `@eqs/platform-nest` dans `apps/api`. Le laisser ne gêne pas : une déclaration continue de l'emporter sur la déduction. Les quatre tests neufs portent sur un workspace, parce que les deux existants étaient mono-application. Éprouvés en retirant la recherche dans le workspace : les deux qui comptent rougissent, les autres non. Huit changesets en attente. 0.15.0 quand le flux se tarira.
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#109
No description provided.