porter() retire le **/ et éteint ESLint sur tout src/ : le gabarit lint 12 fichiers sur 51, eqs-demo zéro sous src/ #100

Closed
opened 2026-09-30 18:32:56 +00:00 by nicolas-fryder · 1 comment

Ce qui ne va pas

eqs-satellite-template/eslint.config.mjs restreint les profils du socle à chaque application avec :

const porter = (prefixe, configurations) =>
  configurations.map((c) => {
    if (!c.files && c.ignores) return c
    const motifs = c.files ?? ['**/*.{ts,tsx,js,mjs,cjs}']
    return { ...c, files: motifs.map((m) => `${prefixe}/${m.replace(/^\*\*\//, '')}`) }
  })

Le .replace(/^\*\*\//, '') transforme **/*.ts en *.ts, et le préfixe donne apps/api/*.ts. Dans un glob, * ne traverse pas / : ce motif ne matche que les fichiers directement dans apps/api, jamais apps/api/src/**.

Mesuré, sur les trois dépôts

Le gabarit lui-même (eqs-satellite-template, eslint . --format json --no-cache) :

12 fichiers analysés — pour 51 fichiers .ts/.tsx sous apps/
  .claude/refuser-genere.mjs        apps/api/vitest.config.mts
  .claude/typer-apres-ecriture.mjs  apps/api/vitest.integration.mts
  apps/api/prisma.config.ts         apps/web/next.config.mjs
  apps/api/scripts/migrer.mjs       apps/web/scripts/next.mjs
  apps/api/scripts/schema-auth.mjs  eslint.config.mjs

apps/api/src/main.ts n'y est pas. Aucun fichier de src/ n'y est.

eqs-demo : 20 fichiers analysés, 0 sous src/. Le dépôt dont la raison d'être est d'appeler chaque package en conditions réelles ne lint pas une ligne de son domaine.

nuage (rétrofit en cours, même porter recopié) : 28 fichiers, 0 violation. En retirant le seul .replace(...) : 227 fichiers, 374 violations.

Pourquoi ça passe inaperçu

Deux effets se combinent, et chacun rassure :

  1. ESLint 9 lint les .js/.mjs/.cjs par défaut, sans qu'aucun files ne les réclame. Les scripts et les fichiers de configuration remontent donc dans la sortie, et la commande n'a pas l'air vide. Ce sont eux, les 12 et les 20.
  2. Zéro violation se lit comme une réussite. Une porte qui rend vert sur du vide est le pire mode d'échec : c'est le constat exact de #83 sur knipSatellite(), écrit il y a huit jours, pour la même raison — un motif déduit qui ne couvre pas la forme réelle du dépôt.

Le commentaire au-dessus de porter() explique longuement pourquoi il faut PRÉFIXER et non remplacer, et il a raison. C'est le .replace qui annule le préfixage qu'il défend.

Le correctif

Préfixer, et ne rien retirer :

return { ...c, files: motifs.map((m) => `${prefixe}/${m}`) }

**/*.ts → apps/api/**/*.ts. Le bloc restreint à **/domain/**/*.ts devient apps/api/**/domain/**/*.ts, ce qui est exactement ce que le commentaire veut préserver.

Ce que je proposerais au-delà du correctif

Exporter porter depuis @eqs/tooling/eslint. Trois dépôts portent aujourd'hui la même fonction recopiée à l'identique, avec le même défaut — c'est la définition de ce qu'un socle existe pour supprimer (« six dépôts ont copié check.mjs, aucune copie n'est identique »). Un satellite qui écrit ...porter('apps/api', nest) ne devrait pas avoir à connaître la sémantique des globs d'ESLint.

Et un contrôle qui refuse une porte vide. tooling.eslint-etend vérifie que la configuration étend bien les profils ; il ne vérifie pas qu'elle ATTEINT quelque chose. Un eslint --format json qui rend moins de fichiers que le dépôt n'en compte sous src/ est un signal mécanique, et il aurait attrapé celui-ci comme il aurait attrapé #83. C'est la règle du cadre : « une porte qui ment est pire que pas de porte ».

Versions

@eqs/tooling 0.14.0, ESLint 9.39.5, Node 24.15.0, pnpm 11.8.0, Windows 11.
Mesuré le 2026-09-30 sur eqs-satellite-template@7035fec, eqs-demo@2550041, et EQS-Sandbox/NUAGE branche retrofit-socle-eqs.

Contournement posé

nuage écrit son porter() sans le .replace, avec un commentaire qui pointe ce ticket. Il n'y aura rien à retirer quand @eqs/tooling exportera le sien : la fonction locale disparaîtra au profit de l'import.

## Ce qui ne va pas `eqs-satellite-template/eslint.config.mjs` restreint les profils du socle à chaque application avec : ```js const porter = (prefixe, configurations) => configurations.map((c) => { if (!c.files && c.ignores) return c const motifs = c.files ?? ['**/*.{ts,tsx,js,mjs,cjs}'] return { ...c, files: motifs.map((m) => `${prefixe}/${m.replace(/^\*\*\//, '')}`) } }) ``` Le `.replace(/^\*\*\//, '')` transforme `**/*.ts` en `*.ts`, et le préfixe donne **`apps/api/*.ts`**. Dans un glob, `*` ne traverse pas `/` : ce motif ne matche que les fichiers **directement** dans `apps/api`, jamais `apps/api/src/**`. ## Mesuré, sur les trois dépôts **Le gabarit lui-même** (`eqs-satellite-template`, `eslint . --format json --no-cache`) : ``` 12 fichiers analysés — pour 51 fichiers .ts/.tsx sous apps/ .claude/refuser-genere.mjs apps/api/vitest.config.mts .claude/typer-apres-ecriture.mjs apps/api/vitest.integration.mts apps/api/prisma.config.ts apps/web/next.config.mjs apps/api/scripts/migrer.mjs apps/web/scripts/next.mjs apps/api/scripts/schema-auth.mjs eslint.config.mjs ``` **`apps/api/src/main.ts` n'y est pas.** Aucun fichier de `src/` n'y est. **`eqs-demo`** : 20 fichiers analysés, **0 sous `src/`**. Le dépôt dont la raison d'être est d'appeler chaque package en conditions réelles ne lint pas une ligne de son domaine. **`nuage`** (rétrofit en cours, même `porter` recopié) : **28 fichiers, 0 violation**. En retirant le seul `.replace(...)` : **227 fichiers, 374 violations**. ## Pourquoi ça passe inaperçu Deux effets se combinent, et chacun rassure : 1. **ESLint 9 lint les `.js`/`.mjs`/`.cjs` par défaut**, sans qu'aucun `files` ne les réclame. Les scripts et les fichiers de configuration remontent donc dans la sortie, et la commande n'a pas l'air vide. Ce sont eux, les 12 et les 20. 2. **Zéro violation se lit comme une réussite.** Une porte qui rend vert sur du vide est le pire mode d'échec : c'est le constat exact de #83 sur `knipSatellite()`, écrit il y a huit jours, pour la même raison — un motif déduit qui ne couvre pas la forme réelle du dépôt. Le commentaire au-dessus de `porter()` explique longuement pourquoi il faut PRÉFIXER et non remplacer, et il a raison. C'est le `.replace` qui annule le préfixage qu'il défend. ## Le correctif Préfixer, et ne rien retirer : ```js return { ...c, files: motifs.map((m) => `${prefixe}/${m}`) } ``` `**/*.ts` → `apps/api/**/*.ts`. Le bloc restreint à `**/domain/**/*.ts` devient `apps/api/**/domain/**/*.ts`, ce qui est exactement ce que le commentaire veut préserver. ## Ce que je proposerais au-delà du correctif **Exporter `porter` depuis `@eqs/tooling/eslint`.** Trois dépôts portent aujourd'hui la même fonction recopiée à l'identique, avec le même défaut — c'est la définition de ce qu'un socle existe pour supprimer (« six dépôts ont copié `check.mjs`, aucune copie n'est identique »). Un satellite qui écrit `...porter('apps/api', nest)` ne devrait pas avoir à connaître la sémantique des globs d'ESLint. **Et un contrôle qui refuse une porte vide.** `tooling.eslint-etend` vérifie que la configuration étend bien les profils ; il ne vérifie pas qu'elle ATTEINT quelque chose. Un `eslint --format json` qui rend moins de fichiers que le dépôt n'en compte sous `src/` est un signal mécanique, et il aurait attrapé celui-ci comme il aurait attrapé #83. C'est la règle du cadre : « une porte qui ment est pire que pas de porte ». ## Versions `@eqs/tooling` 0.14.0, ESLint 9.39.5, Node 24.15.0, pnpm 11.8.0, Windows 11. Mesuré le 2026-09-30 sur `eqs-satellite-template@7035fec`, `eqs-demo@2550041`, et `EQS-Sandbox/NUAGE` branche `retrofit-socle-eqs`. ## Contournement posé `nuage` écrit son `porter()` sans le `.replace`, avec un commentaire qui pointe ce ticket. Il n'y aura rien à retirer quand `@eqs/tooling` exportera le sien : la fonction locale disparaîtra au profit de l'import.
Author
Owner

Première moitié faite : @eqs/tooling/eslint exporte porter. PR #102, changeset porter-eslint-sous-dossier, D-050. Le ticket reste ouvert pour la seconde moitié — le contrôle qui refuse une porte qui n'atteint rien.

Reproduit avant de corriger

Sur eqs-satellite-template@7035fec, eslint . --format json --no-cache :

avec le .replace     12 fichiers analysés,  0 sous src/,  0 violation
sans le .replace     30 fichiers analysés,  tout src/ inclus

apps/api/src/main.ts y est avec le correctif, pas sans. Je compte 31 fichiers .ts/.tsx sous apps/ hors generated et dist, là où le ticket dit 51 — l'écart vient probablement du périmètre compté, et il ne change rien au verdict.

Un détail qui a son intérêt pour la suite : mon premier compteur « dont sous src/ » rendait 0 même avec le correctif. C'était mon script qui était faux, pas le correctif — un antislash avalé à l'écriture du fichier, donc une classe de caractères qui ne matchait que les chemins en slash avant. C'est la liste des fichiers qui a tranché, pas le compteur. Deux mesures qui se contredisent, on ne croit ni l'une ni l'autre : on regarde la chose.

Ce que porter fait, et ce qu'il refuse

import { porter, nest, next } from '@eqs/tooling/eslint'
export default [...porter('apps/api', nest), ...porter('apps/web', await next())]

Concaténation de préfixe, rien de retiré. **/domain/**/*.ts devient apps/web/**/domain/**/*.ts, ce que le commentaire d'origine voulait préserver. Un bloc ignores-seul reste global. Et un préfixe vide ou terminé par un slash lève, au lieu de produire des motifs faux en silence.

Le test compte les fichiers, il n'assure pas un motif

C'est le point qui compte le plus dans ce ticket. Une assertion sur la chaîne apps/api/**/*.ts passerait avec un motif juste et une sémantique mal comprise — c'est exactement ce qui a manqué pendant trois recopies, puisque le commentaire au-dessus de porter() décrivait correctement ce qu'il fallait faire.

Le test crée un dépôt à deux niveaux (apps/api/racine.ts, apps/api/src/moyen.ts, apps/api/src/profond/bas.ts) et compte ce qu'ESLint atteint. Éprouvé en réinjectant la faute :

✗ porter : motif attendu apps/api/**/*.ts, obtenu "apps/api/*.ts"
✗ porter : découpage fin perdu — "apps/web/domain/**/*.ts"
✗ porter : ESLint n'atteint pas moyen.ts — fichiers vus : ["racine.ts","eslint.config.mjs"]
✗ porter : ESLint n'atteint pas bas.ts — fichiers vus : ["racine.ts","eslint.config.mjs"]

Sur le cahier

§5 disait « fichiers, pas de code » alors que le paquet livrait déjà conformite, knip-satellite, orval et workspaces-dockerfile. La règle était fausse depuis quatre modules, et on l'avait contournée quatre fois sans l'amender. Elle porte maintenant son critère (D-050) : du code ici quand un satellite devrait sinon connaître la sémantique d'un outil tiers pour écrire sa configuration, et se tromper.

Ce qui reste

  1. Le contrôle qui refuse une porte vide — votre seconde proposition, et la plus durable. tooling.eslint-etend vérifie que la configuration étend les profils, pas qu'elle touche des fichiers. Un eslint --format json qui rend moins de fichiers que le dépôt n'en compte sous src/ aurait attrapé celui-ci et #83. C'est pour ça que le ticket n'est pas fermé.
  2. Les trois dépôts gardent leur copie : leur correctif est d'importer celui-ci, et il se fait chez eux une fois la version publiée. Le gabarit a été mesuré puis remis d'aplomb sans être touché — un correctif d'une ligne qui serait remplacé par l'import à la version suivante vaut moins qu'un seul commit propre.

Trouvé en commitant, même forme que ce ticket

Le pre-commit a refusé ma première tentative pour une variable morte dans mon propre test — à raison. Et au passage il a montré que pnpm check ne lint pas packages/*/scripts/ : lint:racine ne couvre que knip.ts et scripts/ de la racine, et le check de @eqs/tooling joue son vérificateur sans se linter lui-même. Le seul filet est l'ESLint large du pre-commit.

Une porte qui n'atteint pas ce qu'on croit, encore. Pas ouvert en ticket séparé pour l'instant : c'est la même classe que celui-ci, et votre proposition 2 est le bon endroit pour la traiter.

Première moitié faite : `@eqs/tooling/eslint` exporte `porter`. PR #102, changeset `porter-eslint-sous-dossier`, D-050. **Le ticket reste ouvert pour la seconde moitié** — le contrôle qui refuse une porte qui n'atteint rien. ## Reproduit avant de corriger Sur `eqs-satellite-template@7035fec`, `eslint . --format json --no-cache` : ``` avec le .replace 12 fichiers analysés, 0 sous src/, 0 violation sans le .replace 30 fichiers analysés, tout src/ inclus ``` `apps/api/src/main.ts` y est avec le correctif, pas sans. Je compte **31** fichiers `.ts`/`.tsx` sous `apps/` hors `generated` et `dist`, là où le ticket dit 51 — l'écart vient probablement du périmètre compté, et il ne change rien au verdict. Un détail qui a son intérêt pour la suite : mon premier compteur « dont sous `src/` » rendait **0 même avec le correctif**. C'était mon script qui était faux, pas le correctif — un antislash avalé à l'écriture du fichier, donc une classe de caractères qui ne matchait que les chemins en slash avant. C'est la liste des fichiers qui a tranché, pas le compteur. Deux mesures qui se contredisent, on ne croit ni l'une ni l'autre : on regarde la chose. ## Ce que `porter` fait, et ce qu'il refuse ```js import { porter, nest, next } from '@eqs/tooling/eslint' export default [...porter('apps/api', nest), ...porter('apps/web', await next())] ``` Concaténation de préfixe, rien de retiré. `**/domain/**/*.ts` devient `apps/web/**/domain/**/*.ts`, ce que le commentaire d'origine voulait préserver. Un bloc `ignores`-seul reste global. Et un préfixe vide ou terminé par un slash **lève**, au lieu de produire des motifs faux en silence. ## Le test compte les fichiers, il n'assure pas un motif C'est le point qui compte le plus dans ce ticket. Une assertion sur la chaîne `apps/api/**/*.ts` passerait avec un motif juste et une sémantique mal comprise — c'est exactement ce qui a manqué pendant trois recopies, puisque le commentaire au-dessus de `porter()` décrivait correctement ce qu'il fallait faire. Le test crée un dépôt à deux niveaux (`apps/api/racine.ts`, `apps/api/src/moyen.ts`, `apps/api/src/profond/bas.ts`) et compte ce qu'ESLint atteint. Éprouvé en réinjectant la faute : ``` ✗ porter : motif attendu apps/api/**/*.ts, obtenu "apps/api/*.ts" ✗ porter : découpage fin perdu — "apps/web/domain/**/*.ts" ✗ porter : ESLint n'atteint pas moyen.ts — fichiers vus : ["racine.ts","eslint.config.mjs"] ✗ porter : ESLint n'atteint pas bas.ts — fichiers vus : ["racine.ts","eslint.config.mjs"] ``` ## Sur le cahier §5 disait « fichiers, pas de code » alors que le paquet livrait déjà `conformite`, `knip-satellite`, `orval` et `workspaces-dockerfile`. La règle était fausse depuis quatre modules, et on l'avait contournée quatre fois sans l'amender. Elle porte maintenant son critère (D-050) : du code ici quand un satellite devrait sinon connaître la sémantique d'un outil tiers pour écrire sa configuration, et se tromper. ## Ce qui reste 1. **Le contrôle qui refuse une porte vide** — votre seconde proposition, et la plus durable. `tooling.eslint-etend` vérifie que la configuration étend les profils, pas qu'elle touche des fichiers. Un `eslint --format json` qui rend moins de fichiers que le dépôt n'en compte sous `src/` aurait attrapé celui-ci **et** #83. C'est pour ça que le ticket n'est pas fermé. 2. **Les trois dépôts** gardent leur copie : leur correctif est d'importer celui-ci, et il se fait chez eux une fois la version publiée. Le gabarit a été mesuré puis remis d'aplomb sans être touché — un correctif d'une ligne qui serait remplacé par l'import à la version suivante vaut moins qu'un seul commit propre. ## Trouvé en commitant, même forme que ce ticket Le `pre-commit` a refusé ma première tentative pour une variable morte dans mon propre test — à raison. Et au passage il a montré que **`pnpm check` ne lint pas `packages/*/scripts/`** : `lint:racine` ne couvre que `knip.ts` et `scripts/` de la racine, et le `check` de `@eqs/tooling` joue son vérificateur sans se linter lui-même. Le seul filet est l'ESLint large du `pre-commit`. Une porte qui n'atteint pas ce qu'on croit, encore. Pas ouvert en ticket séparé pour l'instant : c'est la même classe que celui-ci, et votre proposition 2 est le bon endroit pour la traiter.
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#100
No description provided.