defineEqsClient produit un client qui ne compile pas : operationName dérivé d'un operationId en ressource.action donne un identifiant TS invalide #34

Closed
opened 2026-09-09 17:54:02 +00:00 by nicolas-fryder · 7 comments

Constaté le 2026-09-09 sur le premier satellite réel monté depuis eqs-satellite-template, au moment d'écrire le premier consommateur de sa propre API. @eqs/* 0.7.0, pnpm 11.8.0.

AGENTS.md est catégorique : « Les APIs internes se consomment via les clients générés (@eqs/*-client). Jamais de fetch direct. » Le contrôle http.fetch-brut le vérifie. Mais aucun outil du socle ne génère de client, et le gabarit n'en livre pas la configuration.

Ce qui existe, et ce qui manque

Ce qui existe :

  • @eqs/http — « Le mutator Orval : auth, traceparent, identité du client, timeout, ApiError ». Il exporte eqsMutator, creerMutator(options), defineTarget(cible), identiteClient(nom, version). La pièce d'exécution est donc prête.
  • main.ts du gabarit commente longuement defineTarget et dit que « le mutator d'Orval y ajoute le jeton, traceparent, le délai d'attente et le plafond de réponse ».
  • openapi.json, committé, dérivé du code.

Ce qui manque :

  • Aucune commande de génération dans la CLI. eqs --help liste doctor, audit, inspect, baseline, openapi export, auth roles, auth entra-manifest, migrate, topology, e2e, login, coolify verify. Rien pour les clients.
  • Aucune configuration Orval dans le gabarit : pas d'orval.config.ts, aucune mention d'Orval hors du commentaire de main.ts.
  • Orval n'est pas installé : absent des dépendances du gabarit comme de celles des @eqs/*.

Reproduction

pnpm exec eqs --help          # aucune commande de génération
grep -rn orval --include='*.json' --include='*.ts' .   # seul résultat : la description de @eqs/http

Ce que ça coûte

Un satellite qui veut consommer une API — la sienne depuis son front, ou celle d'un autre service — n'a que trois issues, et les trois sont mauvaises :

  1. écrire un fetch direct, ce que la doctrine interdit et qu'un contrôle refuse ;
  2. inventer sa propre configuration Orval, ce qui donnera trente configurations différentes dans trente dépôts — exactement ce que le socle existe pour éviter ;
  3. ne pas consommer d'API.

C'est bloquant pour nous : le lot en cours prévoit une façade MCP et un front qui lisent tous deux l'API du service, et la règle « jamais de fetch direct » ne laisse pas le choix.

Un point qui mérite peut-être d'être tranché au passage : la formulation @eqs/*-client suggère qu'un client est publié par le service qu'il dessert (hq-client, atlas-client). Si c'est le modèle, alors le cas « je consomme ma propre API depuis mon propre front » n'est traité nulle part — or c'est le cas le plus courant, et le premier que rencontre un satellite.

Ce que je proposerais

  1. Une commande eqs client generate qui lit openapi.json (ou une cible déclarée) et produit le client avec le mutator de @eqs/http déjà branché. C'est le geste symétrique d'eqs openapi export, et il manque visiblement.
  2. À défaut, une configuration Orval de référence livrée par le gabarit, avec le mutator câblé et le dossier de sortie sous **/generated/** (déjà couvert par .prettierignore et par la règle « on ne modifie pas un fichier généré »).
  3. Écrire quelque part si un satellite doit publier un client de sa propre API, et sous quel nom. Aujourd'hui la question n'a pas de réponse.

Contournement en place

Nous allons écrire une configuration Orval dans le satellite et générer le client sous **/generated/**, avec eqsMutator. Je reviendrai commenter ce ticket avec ce qui aura fonctionné et ce qui aura demandé de deviner — la configuration pourra servir de point de départ si la piste 1 ou 2 est retenue.

Constaté le 2026-09-09 sur le premier satellite réel monté depuis `eqs-satellite-template`, au moment d'écrire le premier consommateur de sa propre API. `@eqs/*` 0.7.0, pnpm 11.8.0. `AGENTS.md` est catégorique : « Les APIs internes se consomment via les clients générés (`@eqs/*-client`). Jamais de `fetch` direct. » Le contrôle `http.fetch-brut` le vérifie. Mais **aucun outil du socle ne génère de client**, et le gabarit n'en livre pas la configuration. ## Ce qui existe, et ce qui manque Ce qui existe : - `@eqs/http` — « Le mutator Orval : auth, traceparent, identité du client, timeout, ApiError ». Il exporte `eqsMutator`, `creerMutator(options)`, `defineTarget(cible)`, `identiteClient(nom, version)`. La pièce d'exécution est donc prête. - `main.ts` du gabarit commente longuement `defineTarget` et dit que « le mutator d'Orval y ajoute le jeton, `traceparent`, le délai d'attente et le plafond de réponse ». - `openapi.json`, committé, dérivé du code. Ce qui manque : - **Aucune commande de génération dans la CLI.** `eqs --help` liste `doctor`, `audit`, `inspect`, `baseline`, `openapi export`, `auth roles`, `auth entra-manifest`, `migrate`, `topology`, `e2e`, `login`, `coolify verify`. Rien pour les clients. - **Aucune configuration Orval dans le gabarit** : pas d'`orval.config.ts`, aucune mention d'Orval hors du commentaire de `main.ts`. - **Orval n'est pas installé** : absent des dépendances du gabarit comme de celles des `@eqs/*`. ## Reproduction ```bash pnpm exec eqs --help # aucune commande de génération grep -rn orval --include='*.json' --include='*.ts' . # seul résultat : la description de @eqs/http ``` ## Ce que ça coûte Un satellite qui veut consommer une API — la sienne depuis son front, ou celle d'un autre service — n'a que trois issues, et les trois sont mauvaises : 1. écrire un `fetch` direct, ce que la doctrine interdit et qu'un contrôle refuse ; 2. inventer sa propre configuration Orval, ce qui donnera **trente configurations différentes** dans trente dépôts — exactement ce que le socle existe pour éviter ; 3. ne pas consommer d'API. C'est bloquant pour nous : le lot en cours prévoit une façade MCP et un front qui lisent tous deux l'API du service, et la règle « jamais de `fetch` direct » ne laisse pas le choix. Un point qui mérite peut-être d'être tranché au passage : la formulation `@eqs/*-client` suggère qu'un client est **publié par le service qu'il dessert** (`hq-client`, `atlas-client`). Si c'est le modèle, alors le cas « je consomme ma propre API depuis mon propre front » n'est traité nulle part — or c'est le cas le plus courant, et le premier que rencontre un satellite. ## Ce que je proposerais 1. **Une commande `eqs client generate`** qui lit `openapi.json` (ou une cible déclarée) et produit le client avec le mutator de `@eqs/http` déjà branché. C'est le geste symétrique d'`eqs openapi export`, et il manque visiblement. 2. **À défaut, une configuration Orval de référence livrée par le gabarit**, avec le mutator câblé et le dossier de sortie sous `**/generated/**` (déjà couvert par `.prettierignore` et par la règle « on ne modifie pas un fichier généré »). 3. **Écrire quelque part si un satellite doit publier un client de sa propre API**, et sous quel nom. Aujourd'hui la question n'a pas de réponse. ## Contournement en place Nous allons écrire une configuration Orval dans le satellite et générer le client sous `**/generated/**`, avec `eqsMutator`. Je reviendrai commenter ce ticket avec ce qui aura fonctionné et ce qui aura demandé de deviner — la configuration pourra servir de point de départ si la piste 1 ou 2 est retenue.
Author
Owner

Le manque est réel, mais le point central du ticket est faux : la configuration Orval existe, elle est publiée, et elle est documentée.

@eqs/tooling/orval exporte defineEqsClient({ input, output }), qui rend la configuration imposée — tags-split, client fetch, mutator @eqs/http, schémas zod, mocks MSW, operationId conservés. Elle est déclarée dans les exports du package, décrite dans packages/tooling/README.md et montrée telle quelle dans packages/http/README.md.

Votre grep ne pouvait pas la voir :

grep -rn orval --include='*.json' --include='*.ts' .

Le fichier est orval.js. Les deux --include l'excluaient. Ça vaut d'être dit, parce que c'est la seule raison pour laquelle le ticket conclut « rien n'existe » là où il fallait conclure « rien ne le branche ».

Ce qui est vrai, vérifié :

  • aucune commande de génération dans la CLI — confirmé sur eqs --help ;
  • orval n'est installé nulle part — ni dans le socle, ni dans le gabarit (grep '"orval"' **/package.json ne rend rien). La configuration existe, le moteur qui la lit n'est pas là ;
  • le gabarit n'a pas d'orval.config.ts, ni de script ;
  • le cas « je consomme ma propre API depuis mon propre front » n'est traité nulle part. Vous avez raison de le pointer comme le plus courant — et il l'est d'autant plus depuis D-041, qui dit qu'entre les deux applications d'un satellite nest, « la seconde consomme la première par son client généré ». Une décision d'aujourd'hui s'appuie donc sur une chaîne qui n'existe pas.

Ce que la vérification a trouvé en plus, et qui vous aurait coûté la journée suivante.

defineEqsClient imposait le mutator serveur :

override: { mutator: { path: '@eqs/http', name: 'eqsMutator' } }

Or @eqs/http dépend de @eqs/auth, qui importe node:async_hooks et node:crypto et tire better-auth. Un client généré avec la configuration imposée ne se bundle pas pour un navigateur. Vous alliez écrire cette configuration, générer, importer le client dans un composant client, et vous heurter à un échec de build dont la cause est trois packages plus loin.

Le socle a pourtant la bonne pièce : creerMutatorNavigateur, dans @eqs/http/browser, qui ne dépend que de @eqs/errors/client, envoie la session par cookie et n'ajoute aucun Authorization — écrit exactement pour les profils front et next-fullstack. Rien ne permettait de le sélectionner.

Corrigé. defineEqsClient prend une option mutator. Et comme creerMutatorNavigateur est une fabrique (elle prend baseUrl et clientName) et non un mutator, la pointer directement est refusé, avec le code à écrire :

defineEqsClient : `creerMutatorNavigateur` est une fabrique, pas un mutator.
  Écrivez un module de VOTRE application qui l’instancie, et pointez-le :

    // src/lib/mutator.ts
    import { creerMutatorNavigateur } from '@eqs/http/browser'
    export const mutator = creerMutatorNavigateur({
      baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '',
      clientName: 'web',
    })

    mutator: { path: './src/lib/mutator.ts', name: 'mutator' }

Les trois cas — le défaut, le choix, le refus — sont couverts par le check de @eqs/tooling.

Deux choses que le gabarit vous dit déjà, et qui répondent à des questions que vous alliez vous poser : son .gitignore porte apps/api/generated/ et apps/web/generated/. Donc l'emplacement de sortie attendu est apps/<app>/generated/, et la sortie n'est pas committée — elle se régénère. Corollaire dont il faut tenir compte : sur un clone neuf, pnpm check tourne avant toute génération, donc une application qui importe son client généré ne compile pas tant que la génération n'est pas dans la chaîne. C'est probablement pour ça que rien n'est branché aujourd'hui, et c'est le vrai travail restant.

Sur votre piste 1, eqs client generate : je ne pense pas que ce soit la bonne, et je préfère l'argumenter que la faire. eqs openapi export n'implémente rien — il appelle l'export du dépôt. Le geste symétrique n'est pas d'embarquer Orval dans la CLI (une grosse dépendance, pour tous les dépôts, y compris ceux qui ne génèrent aucun client) mais que le dépôt lance orval avec la configuration partagée. C'est votre piste 2, et c'est celle qui est cohérente avec le reste.

Je laisse ce ticket ouvert. Ce qui est corrigé, c'est le défaut que j'ai trouvé dedans, pas le manque que vous signalez. Deux décisions restent à prendre, et elles ne m'appartiennent pas :

  1. Un satellite publie-t-il un client de sa propre API ? Le plan §3.8 dit que les clients sont des packages npm versionnés publiés au registre (@eqs/hq-client) — mais D-041, écrite aujourd'hui, dit qu'un satellite ne produit aucun package publiable. Les deux ne tiennent pas ensemble. Soit D-041 gagne et un consommateur génère son client depuis le contrat committé du fournisseur, soit le client généré devient l'exception à D-041.
  2. La génération entre-t-elle dans la chaîne (postinstall, prebuild) puisque la sortie est ignorée par git, ou la sortie devient-elle committée comme openapi.json ?

Votre retour d'expérience nous intéresse : vous êtes en train d'écrire la configuration, et ce qui vous aura demandé de deviner dira ce que le gabarit doit livrer. Postez-la ici quand elle marchera.

Branche fix/tickets-socle-du-premier-satellite.

Le manque est réel, mais **le point central du ticket est faux : la configuration Orval existe, elle est publiée, et elle est documentée.** `@eqs/tooling/orval` exporte `defineEqsClient({ input, output })`, qui rend la configuration imposée — `tags-split`, client `fetch`, mutator `@eqs/http`, schémas zod, mocks MSW, `operationId` conservés. Elle est déclarée dans les `exports` du package, décrite dans `packages/tooling/README.md` et montrée telle quelle dans `packages/http/README.md`. Votre `grep` ne pouvait pas la voir : ```bash grep -rn orval --include='*.json' --include='*.ts' . ``` Le fichier est `orval.js`. Les deux `--include` l'excluaient. Ça vaut d'être dit, parce que c'est la seule raison pour laquelle le ticket conclut « rien n'existe » là où il fallait conclure « rien ne le branche ». **Ce qui est vrai, vérifié :** - **aucune commande de génération dans la CLI** — confirmé sur `eqs --help` ; - **`orval` n'est installé nulle part** — ni dans le socle, ni dans le gabarit (`grep '"orval"' **/package.json` ne rend rien). La configuration existe, le moteur qui la lit n'est pas là ; - **le gabarit n'a pas d'`orval.config.ts`**, ni de script ; - **le cas « je consomme ma propre API depuis mon propre front » n'est traité nulle part.** Vous avez raison de le pointer comme le plus courant — et il l'est d'autant plus depuis D-041, qui dit qu'entre les deux applications d'un satellite `nest`, « la seconde consomme la première par son client généré ». Une décision d'aujourd'hui s'appuie donc sur une chaîne qui n'existe pas. **Ce que la vérification a trouvé en plus, et qui vous aurait coûté la journée suivante.** `defineEqsClient` imposait le mutator **serveur** : ```js override: { mutator: { path: '@eqs/http', name: 'eqsMutator' } } ``` Or `@eqs/http` dépend de `@eqs/auth`, qui importe `node:async_hooks` et `node:crypto` et tire `better-auth`. **Un client généré avec la configuration imposée ne se bundle pas pour un navigateur.** Vous alliez écrire cette configuration, générer, importer le client dans un composant client, et vous heurter à un échec de build dont la cause est trois packages plus loin. Le socle a pourtant la bonne pièce : `creerMutatorNavigateur`, dans `@eqs/http/browser`, qui ne dépend que de `@eqs/errors/client`, envoie la session par cookie et n'ajoute aucun `Authorization` — écrit exactement pour les profils `front` et `next-fullstack`. Rien ne permettait de le sélectionner. **Corrigé.** `defineEqsClient` prend une option `mutator`. Et comme `creerMutatorNavigateur` est une **fabrique** (elle prend `baseUrl` et `clientName`) et non un mutator, la pointer directement est refusé, avec le code à écrire : ``` defineEqsClient : `creerMutatorNavigateur` est une fabrique, pas un mutator. Écrivez un module de VOTRE application qui l’instancie, et pointez-le : // src/lib/mutator.ts import { creerMutatorNavigateur } from '@eqs/http/browser' export const mutator = creerMutatorNavigateur({ baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '', clientName: 'web', }) mutator: { path: './src/lib/mutator.ts', name: 'mutator' } ``` Les trois cas — le défaut, le choix, le refus — sont couverts par le `check` de `@eqs/tooling`. **Deux choses que le gabarit vous dit déjà**, et qui répondent à des questions que vous alliez vous poser : son `.gitignore` porte `apps/api/generated/` et `apps/web/generated/`. Donc l'emplacement de sortie attendu est `apps/<app>/generated/`, et **la sortie n'est pas committée** — elle se régénère. Corollaire dont il faut tenir compte : sur un clone neuf, `pnpm check` tourne avant toute génération, donc une application qui importe son client généré ne compile pas tant que la génération n'est pas dans la chaîne. C'est probablement pour ça que rien n'est branché aujourd'hui, et c'est le vrai travail restant. **Sur votre piste 1, `eqs client generate` : je ne pense pas que ce soit la bonne**, et je préfère l'argumenter que la faire. `eqs openapi export` n'implémente rien — il appelle l'export du dépôt. Le geste symétrique n'est pas d'embarquer Orval dans la CLI (une grosse dépendance, pour tous les dépôts, y compris ceux qui ne génèrent aucun client) mais que le dépôt lance `orval` avec la configuration partagée. C'est votre piste 2, et c'est celle qui est cohérente avec le reste. **Je laisse ce ticket ouvert.** Ce qui est corrigé, c'est le défaut que j'ai trouvé dedans, pas le manque que vous signalez. Deux décisions restent à prendre, et elles ne m'appartiennent pas : 1. **Un satellite publie-t-il un client de sa propre API ?** Le plan §3.8 dit que les clients sont des packages npm versionnés publiés au registre (`@eqs/hq-client`) — mais D-041, écrite aujourd'hui, dit qu'un satellite ne produit aucun package publiable. Les deux ne tiennent pas ensemble. Soit D-041 gagne et un consommateur génère son client depuis le contrat committé du fournisseur, soit le client généré devient l'exception à D-041. 2. **La génération entre-t-elle dans la chaîne** (`postinstall`, `prebuild`) puisque la sortie est ignorée par git, ou la sortie devient-elle committée comme `openapi.json` ? Votre retour d'expérience nous intéresse : vous êtes en train d'écrire la configuration, et ce qui vous aura demandé de deviner dira ce que le gabarit doit livrer. Postez-la ici quand elle marchera. Branche `fix/tickets-socle-du-premier-satellite`.
nicolas-fryder changed title from La doctrine impose les clients générés, mais rien ne les génère : ni commande CLI, ni configuration Orval livrée to defineEqsClient produit un client qui ne compile pas : operationName dérivé d'un operationId en ressource.action donne un identifiant TS invalide 2026-09-09 18:30:19 +00:00
Author
Owner

Rectification — je me suis trompé sur la prémisse, et le vrai sujet est ailleurs

Le corps ci-dessus est faux sur un point central : @eqs/tooling/orval existe et exporte defineEqsClient({ input, output, mocks? }). J'avais cherché « orval » dans les fichiers du dépôt et dans la CLI, pas dans @eqs/tooling. Toutes mes excuses pour le temps que ça a pu coûter.

Le titre est corrigé. Ce qui suit remplace la section « Ce qui manque ».

Le vrai sujet : defineEqsClient produit, tel que documenté, un client qui ne compile pas. Quatre points, trouvés en le jouant contre un contrat réel de 12 opérations.

1. operationName casse pour tout operationId de la flotte

defineEqsClient fixe override.operationName: (operation) => operation.operationId. Or D-016 impose ressource.action — donc un point. Orval en fait des identifiants TypeScript, et noeud.creer n'est pas un identifiant valide.

Ce n'est pas notre cas particulier : c'est vrai de toute opération conforme à la convention du socle. Le générateur et la règle de nommage se contredisent.

Contournement appliqué : delete configuration.client.output.override.operationName, ce qui laisse Orval dériver ses noms du chemin et de la méthode.

2. Le mutator @eqs/http est refusé sans inferred: true

Sans ce drapeau, la sortie porte une enveloppe de trop et ne type-check pas. Correctif : mutator: { …, inferred: true }.

3. Les schémas générés sont importés sans extension, illégal en ESM strict

Le fichier de schémas séparé (celui que defineEqsClient configure) est importé sans .js, ce que le "type": "module" du gabarit refuse à l'exécution.

4. Pas de baseUrl, donc eqsMutator ne reconnaît aucune cible

Les fonctions générées produisent des URL relatives. Or eqsMutator associe une cible — et donc le jeton, traceparent, le délai d'attente — par égalité littérale d'un baseUrl absolu. Sans lui, aucune cible ne correspond et rien n'est ajouté à la requête : le mutator est là, mais inerte.

Contournement : configuration.client.output.baseUrl = { runtime: 'process.env.PILOTAGE_API_URL' }.

Ce que ça donne aujourd'hui

Une configuration qui marche, mais qui défait par mutation d'objet trois des réglages que defineEqsClient vient de poser :

import { defineEqsClient } from '@eqs/tooling/orval'

const configuration = defineEqsClient({ input: '../../openapi.json', output: 'src/generated/api' })

delete configuration.client.output.override.operationName
configuration.client.output.override.mutator.inferred = true
configuration.client.output.baseUrl = { runtime: 'process.env.PILOTAGE_API_URL' }

export default configuration

C'est laid, et surtout fragile : ça dépend de la forme interne de ce que rend defineEqsClient. Trente dépôts qui copient ces trois lignes, c'est trente ruptures à la prochaine évolution du helper.

Ce que je proposerais, vu d'ici

  • Pour le point 1, trancher entre deux voies : soit defineEqsClient assainit l'operationId (noeud.creer → noeudCreer), soit il ne fixe pas operationName du tout. La première garde des noms lisibles et alignés sur le contrat ; je la préférerais.
  • Pour les points 2 et 3, ce sont des réglages à poser par défaut dans le helper.
  • Pour le point 4, baseUrl mériterait d'être un paramètre de defineEqsClient — le nom de la variable d'environnement est propre à chaque satellite, mais le fait qu'il en faille une est général.

Le détail complet, avec les commandes jouées et les sorties, est dans le rapport de la tâche ; je peux le coller ici si c'est utile au traitement en cours.

Comme le ticket est en cours de traitement : notre configuration locale vit dans apps/mcp/orval.config.ts avec un commentaire renvoyant ici. Dites-moi quand la correction est publiée, on la reprendra et on retirera les trois lignes de mutation — c'est exactement le genre de contournement qu'on veut voir disparaître.

## Rectification — je me suis trompé sur la prémisse, et le vrai sujet est ailleurs **Le corps ci-dessus est faux sur un point central : `@eqs/tooling/orval` existe et exporte `defineEqsClient({ input, output, mocks? })`.** J'avais cherché « orval » dans les fichiers du dépôt et dans la CLI, pas dans `@eqs/tooling`. Toutes mes excuses pour le temps que ça a pu coûter. Le titre est corrigé. Ce qui suit remplace la section « Ce qui manque ». **Le vrai sujet : `defineEqsClient` produit, tel que documenté, un client qui ne compile pas.** Quatre points, trouvés en le jouant contre un contrat réel de 12 opérations. ### 1. `operationName` casse pour tout `operationId` de la flotte `defineEqsClient` fixe `override.operationName: (operation) => operation.operationId`. Or D-016 impose `ressource.action` — donc un **point**. Orval en fait des identifiants TypeScript, et `noeud.creer` n'est pas un identifiant valide. Ce n'est pas notre cas particulier : c'est vrai de **toute** opération conforme à la convention du socle. Le générateur et la règle de nommage se contredisent. Contournement appliqué : `delete configuration.client.output.override.operationName`, ce qui laisse Orval dériver ses noms du chemin et de la méthode. ### 2. Le mutator `@eqs/http` est refusé sans `inferred: true` Sans ce drapeau, la sortie porte une enveloppe de trop et ne type-check pas. Correctif : `mutator: { …, inferred: true }`. ### 3. Les schémas générés sont importés sans extension, illégal en ESM strict Le fichier de schémas séparé (celui que `defineEqsClient` configure) est importé sans `.js`, ce que le `"type": "module"` du gabarit refuse à l'exécution. ### 4. Pas de `baseUrl`, donc `eqsMutator` ne reconnaît aucune cible Les fonctions générées produisent des URL relatives. Or `eqsMutator` associe une cible — et donc le jeton, `traceparent`, le délai d'attente — par **égalité littérale d'un `baseUrl` absolu**. Sans lui, aucune cible ne correspond et rien n'est ajouté à la requête : le mutator est là, mais inerte. Contournement : `configuration.client.output.baseUrl = { runtime: 'process.env.PILOTAGE_API_URL' }`. ### Ce que ça donne aujourd'hui Une configuration qui marche, mais qui **défait par mutation d'objet** trois des réglages que `defineEqsClient` vient de poser : ```ts import { defineEqsClient } from '@eqs/tooling/orval' const configuration = defineEqsClient({ input: '../../openapi.json', output: 'src/generated/api' }) delete configuration.client.output.override.operationName configuration.client.output.override.mutator.inferred = true configuration.client.output.baseUrl = { runtime: 'process.env.PILOTAGE_API_URL' } export default configuration ``` C'est laid, et surtout fragile : ça dépend de la forme interne de ce que rend `defineEqsClient`. Trente dépôts qui copient ces trois lignes, c'est trente ruptures à la prochaine évolution du helper. ### Ce que je proposerais, vu d'ici - Pour le point 1, trancher entre deux voies : soit `defineEqsClient` **assainit** l'`operationId` (`noeud.creer` → `noeudCreer`), soit il ne fixe pas `operationName` du tout. La première garde des noms lisibles et alignés sur le contrat ; je la préférerais. - Pour les points 2 et 3, ce sont des réglages à poser par défaut dans le helper. - Pour le point 4, `baseUrl` mériterait d'être un paramètre de `defineEqsClient` — le nom de la variable d'environnement est propre à chaque satellite, mais le fait qu'il en faille une est général. Le détail complet, avec les commandes jouées et les sorties, est dans le rapport de la tâche ; je peux le coller ici si c'est utile au traitement en cours. **Comme le ticket est en cours de traitement** : notre configuration locale vit dans `apps/mcp/orval.config.ts` avec un commentaire renvoyant ici. Dites-moi quand la correction est publiée, on la reprendra et on retirera les trois lignes de mutation — c'est exactement le genre de contournement qu'on veut voir disparaître.
Author
Owner

Suite, et fin : j'ai câblé la chaîne pour de vrai sur le gabarit. Elle ne marchait pas du tout, et pour deux raisons de plus que celle du commentaire précédent.

Défaut 2 — le nom des fonctions générées n'était pas du JavaScript. defineEqsClient passait l'operationId tel quel à Orval. Or un operationId porte un point (ping.creer, D-006), qui ne peut pas figurer dans un identifiant :

export type ping.lireResponse200 = { … }
export const ping.lire = async ( options?: RequestInit) => { … }

Mesuré sur le gabarit, pour trois ressources : 137 erreurs, dont error TS1005: '=' expected. Le code produit ne parse même pas.

Les fonctions s'appellent maintenant pingLire, pingCreer, devisLigneAjouter. L'operationId lui-même ne bouge pas — il reste la clé de corrélation dans le contrat, dans roles.yaml et dans le garde ; ce qui change est le nom sous lequel le code appelle la route.

Défaut 3 — les deux mutators avaient la mauvaise signature. Ils rendaient Promise<ReponseHttp<T>>. Le client fetch d'Orval, lui, appelle le mutator avec le type de la réponse entière — une union discriminée par le statut, intersectée avec { headers } — et attend ce type-là en retour. Ils enveloppaient donc une seconde fois :

Type 'ReponseHttp<pingLireResponse>' is not assignable to type 'pingLireResponse'

Le plus parlant : le commentaire de ReponseHttp disait « Ce qu'Orval (client fetch) attend en retour ». C'est exactement la phrase qui était fausse, et personne ne pouvait s'en apercevoir — les tests du package appelaient les mutators à la main, avec le type du corps, et passaient.

packages/http/test/contrat-orval.test.ts recopie désormais la forme qu'Orval émet et vérifie les deux mutators à la compilation. C'est la barrière qui manquait.

Mesure finale, sur le gabarit : 137 → 9 → 4 → 0. pnpm check vert, avec le front qui appelle réellement pingCreer.

Vos deux questions sont tranchées — D-044.

Le client se génère chez celui qui appelle, et son code est committé.

Le raisonnement, court : ce qui doit être partagé l'est déjà — la configuration (@eqs/tooling/orval) et le transport (@eqs/http). Un client entièrement généré n'a rien en propre à déboguer ; un défaut de transport se corrige une fois, dans le socle. Publier depuis eqs-platform était exclu par le versionnement commun (D-022) : @eqs/atlas-client aurait changé de version à chaque correction dans @eqs/errors. Un dépôt dédié tenait, mais faisait de la publication un passage obligé — une API qui bouge bloque ses consommateurs jusqu'à ce que quelqu'un y republie.

Et le code produit est committé, pas ignoré : ignoré, il n'existe pas sur un clone neuf, et une application qui importe son client ne compile pas avant que la génération ait tourné — donc avant pnpm check.

Ce que le gabarit livre maintenant (branche feat/client-genere) :

  • orval en dépendance de développement ;
  • orval.config.mjs — en .mjs et pas .ts : Orval lit une config TypeScript avec esbuild, dont le script d'installation télécharge un binaire, qu'on n'accorde pas pour un confort d'écriture ;
  • apps/web/src/lib/mutator.ts, qui instancie le mutator navigateur ;
  • pnpm client, à jouer après pnpm openapi ;
  • le client produit, committé, et la page d'accueil qui l'appelle — pour que le montage soit exercé par la porte au lieu d'être documenté et jamais joué.

Ordre à respecter chez vous : cette branche du gabarit exige la prochaine version de @eqs/*. Contre 0.7.0 elle porte 5 erreurs, et le pre-push la refuse — ce qui est le comportement voulu. Après la sortie du socle : relever les bornes @eqs/*, pnpm client, pnpm check.

Je ferme : le manque est comblé et les décisions sont écrites. Si la génération vous résiste malgré ça, rouvrez avec la sortie — vous êtes le premier à l'emprunter pour de vrai, et les trois défauts ci-dessus montrent que ce chemin n'avait jamais servi.

Suite, et fin : j'ai câblé la chaîne pour de vrai sur le gabarit. **Elle ne marchait pas du tout**, et pour deux raisons de plus que celle du commentaire précédent. **Défaut 2 — le nom des fonctions générées n'était pas du JavaScript.** `defineEqsClient` passait l'operationId tel quel à Orval. Or un operationId porte un point (`ping.creer`, D-006), qui ne peut pas figurer dans un identifiant : ```ts export type ping.lireResponse200 = { … } export const ping.lire = async ( options?: RequestInit) => { … } ``` Mesuré sur le gabarit, pour trois ressources : **137 erreurs**, dont `error TS1005: '=' expected`. Le code produit ne parse même pas. Les fonctions s'appellent maintenant `pingLire`, `pingCreer`, `devisLigneAjouter`. **L'operationId lui-même ne bouge pas** — il reste la clé de corrélation dans le contrat, dans `roles.yaml` et dans le garde ; ce qui change est le nom sous lequel le code appelle la route. **Défaut 3 — les deux mutators avaient la mauvaise signature.** Ils rendaient `Promise<ReponseHttp<T>>`. Le client `fetch` d'Orval, lui, appelle le mutator avec le type de la **réponse entière** — une union discriminée par le statut, intersectée avec `{ headers }` — et attend ce type-là en retour. Ils enveloppaient donc une seconde fois : ``` Type 'ReponseHttp<pingLireResponse>' is not assignable to type 'pingLireResponse' ``` Le plus parlant : le commentaire de `ReponseHttp` disait « Ce qu'Orval (client `fetch`) attend en retour ». C'est exactement la phrase qui était fausse, et personne ne pouvait s'en apercevoir — les tests du package appelaient les mutators à la main, avec le type du corps, et passaient. `packages/http/test/contrat-orval.test.ts` recopie désormais la forme qu'Orval émet et vérifie les deux mutators à la compilation. C'est la barrière qui manquait. **Mesure finale, sur le gabarit : 137 → 9 → 4 → 0.** `pnpm check` vert, avec le front qui appelle réellement `pingCreer`. **Vos deux questions sont tranchées — D-044.** *Le client se génère chez celui qui appelle, et son code est committé.* Le raisonnement, court : ce qui doit être partagé l'est déjà — la configuration (`@eqs/tooling/orval`) et le transport (`@eqs/http`). Un client entièrement généré n'a rien en propre à déboguer ; un défaut de transport se corrige une fois, dans le socle. Publier depuis `eqs-platform` était exclu par le versionnement commun (D-022) : `@eqs/atlas-client` aurait changé de version à chaque correction dans `@eqs/errors`. Un dépôt dédié tenait, mais faisait de la publication un passage obligé — une API qui bouge bloque ses consommateurs jusqu'à ce que quelqu'un y republie. Et le code produit est **committé**, pas ignoré : ignoré, il n'existe pas sur un clone neuf, et une application qui importe son client ne compile pas avant que la génération ait tourné — donc avant `pnpm check`. **Ce que le gabarit livre maintenant** (branche `feat/client-genere`) : - `orval` en dépendance de développement ; - `orval.config.mjs` — en `.mjs` et pas `.ts` : Orval lit une config TypeScript avec esbuild, dont le script d'installation télécharge un binaire, qu'on n'accorde pas pour un confort d'écriture ; - `apps/web/src/lib/mutator.ts`, qui instancie le mutator navigateur ; - `pnpm client`, à jouer après `pnpm openapi` ; - le client produit, committé, et la page d'accueil qui l'appelle — pour que le montage soit exercé par la porte au lieu d'être documenté et jamais joué. **Ordre à respecter chez vous** : cette branche du gabarit exige la prochaine version de `@eqs/*`. Contre 0.7.0 elle porte 5 erreurs, et le `pre-push` la refuse — ce qui est le comportement voulu. Après la sortie du socle : relever les bornes `@eqs/*`, `pnpm client`, `pnpm check`. Je ferme : le manque est comblé et les décisions sont écrites. Si la génération vous résiste malgré ça, rouvrez avec la sortie — vous êtes le premier à l'emprunter pour de vrai, et les trois défauts ci-dessus montrent que ce chemin n'avait jamais servi.
Author
Owner

Cinquième défaut, et il est structurel : exactOptionalPropertyTypes contre les paramètres optionnels

Trouvé après coup, en branchant la façade sur une route qui a un paramètre optionnel. Utile si le traitement est encore en cours, parce qu'il ne se corrige pas au même endroit que les quatre autres.

@eqs/tooling/tsconfig.base.json active "exactOptionalPropertyTypes": true. Sous ce réglage, { p?: string } n'accepte pas une valeur explicitement undefined : il faut { p?: string | undefined }.

Or le client généré déclare :

export type VuePretMaintenantParams = {
  projetId: string;
  personneId?: string;      // sans « | undefined »
};

Et l'appelant, lui, a presque toujours un string | undefined en main — ici le schéma Zod généré par la même passe Orval (ZodOptional<ZodString>) rend exactement ça. D'où :

src/main.ts(122,46): error TS2379: Argument of type 'ShapeOutput<{ projetId: ZodString;
personneId: ZodOptional<ZodString>; }>' is not assignable to parameter of type
'VuePretMaintenantParams' with 'exactOptionalPropertyTypes: true'.
  Types of property 'personneId' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.

Pourquoi c'est structurel et pas anecdotique

Les deux moitiés viennent du socle et se contredisent : @eqs/tooling impose exactOptionalPropertyTypes, et @eqs/tooling/orval génère des types qui ne le respectent pas. Dès qu'une API a un seul paramètre de requête optionnel, son client généré est inutilisable par un consommateur qui compile avec le tsconfig du socle — c'est-à-dire tous.

Et les deux passes d'Orval configurées par defineEqsClient (le client et les schémas Zod) produisent des formes mutuellement incompatibles pour le même paramètre : p?: string d'un côté, p: string | undefined de l'autre. Elles ne peuvent pas se parler.

À noter aussi : ça se déclenche à retardement. Le client compilait tant que le paramètre était obligatoire dans le contrat ; il a cessé de compiler quand on a corrigé le contrat pour rendre le paramètre optionnel — ce qui était la bonne correction. Régénérer le client ne répare rien, puisque le défaut est dans la forme générée.

Ce que je proposerais

orval sait le faire : output.override.useNamedParameters n'est pas le sujet, mais le générateur TypeScript accepte d'émettre | undefined sur les propriétés optionnelles. Si l'option existe dans la version épinglée, la poser dans defineEqsClient règle le cas pour les trente dépôts. Sinon, la question devient : faut-il vraiment exactOptionalPropertyTypes dans le tsconfig de base, sachant que le générateur maison ne le respecte pas ? Les deux se défendent, mais pas ensemble.

Contournement en place

Dans la façade, on construit l'objet de paramètres en omettant la clé plutôt qu'en passant undefined :

const params = personneId === undefined ? { projetId } : { projetId, personneId }

Trois lignes purement techniques, commentées et renvoyant ici. Elles seront à retirer — et il y en aura une par paramètre optionnel de chaque API, dans chaque consommateur, tant que le point n'est pas tranché.

## Cinquième défaut, et il est structurel : `exactOptionalPropertyTypes` contre les paramètres optionnels Trouvé après coup, en branchant la façade sur une route qui a un paramètre optionnel. Utile si le traitement est encore en cours, parce qu'il ne se corrige pas au même endroit que les quatre autres. **`@eqs/tooling/tsconfig.base.json` active `"exactOptionalPropertyTypes": true`.** Sous ce réglage, `{ p?: string }` n'accepte **pas** une valeur explicitement `undefined` : il faut `{ p?: string | undefined }`. Or le client généré déclare : ```ts export type VuePretMaintenantParams = { projetId: string; personneId?: string; // sans « | undefined » }; ``` Et l'appelant, lui, a presque toujours un `string | undefined` en main — ici le schéma Zod généré par la même passe Orval (`ZodOptional<ZodString>`) rend exactement ça. D'où : ``` src/main.ts(122,46): error TS2379: Argument of type 'ShapeOutput<{ projetId: ZodString; personneId: ZodOptional<ZodString>; }>' is not assignable to parameter of type 'VuePretMaintenantParams' with 'exactOptionalPropertyTypes: true'. Types of property 'personneId' are incompatible. Type 'string | undefined' is not assignable to type 'string'. ``` ### Pourquoi c'est structurel et pas anecdotique Les deux moitiés viennent du socle et se contredisent : `@eqs/tooling` impose `exactOptionalPropertyTypes`, et `@eqs/tooling/orval` génère des types qui ne le respectent pas. **Dès qu'une API a un seul paramètre de requête optionnel, son client généré est inutilisable** par un consommateur qui compile avec le tsconfig du socle — c'est-à-dire tous. Et les deux passes d'Orval configurées par `defineEqsClient` (le client et les schémas Zod) produisent des formes **mutuellement incompatibles** pour le même paramètre : `p?: string` d'un côté, `p: string | undefined` de l'autre. Elles ne peuvent pas se parler. À noter aussi : ça se déclenche **à retardement**. Le client compilait tant que le paramètre était obligatoire dans le contrat ; il a cessé de compiler quand on a corrigé le contrat pour rendre le paramètre optionnel — ce qui était la bonne correction. Régénérer le client ne répare rien, puisque le défaut est dans la forme générée. ### Ce que je proposerais `orval` sait le faire : `output.override.useNamedParameters` n'est pas le sujet, mais le générateur TypeScript accepte d'émettre `| undefined` sur les propriétés optionnelles. Si l'option existe dans la version épinglée, la poser dans `defineEqsClient` règle le cas pour les trente dépôts. Sinon, la question devient : faut-il vraiment `exactOptionalPropertyTypes` dans le tsconfig de base, sachant que le générateur maison ne le respecte pas ? Les deux se défendent, mais pas ensemble. ### Contournement en place Dans la façade, on construit l'objet de paramètres en **omettant la clé** plutôt qu'en passant `undefined` : ```ts const params = personneId === undefined ? { projetId } : { projetId, personneId } ``` Trois lignes purement techniques, commentées et renvoyant ici. Elles seront à retirer — et il y en aura une par paramètre optionnel de chaque API, dans chaque consommateur, tant que le point n'est pas tranché.
Author
Owner

Je rouvre : vos deux commentaires arrivent après ma fermeture, et ils portent trois défauts de plus, tous vrais. J'ai vérifié chacun par mes propres moyens et les quatre sont corrigés. Le point sur votre rectification d'abord, puis vos cinq points un par un.

Sur la rectification : pas besoin de vous excuser. Nous avons trouvé le défaut du nom des fonctions chacun de notre côté, dans la même heure — c'est plutôt le signe que le chemin était vraiment neuf.


1. operationName — d'accord, et corrigé

Même mesure que la vôtre. defineEqsClient produit maintenant pingLire, devisLigneAjouter. L'operationId ne bouge pas : il reste la clé de corrélation dans le contrat, dans roles.yaml et dans le garde. C'est votre première proposition, et c'est la bonne pour la raison que vous donniez : les noms restent lisibles et alignés sur le contrat.

Votre delete configuration.client.output.override.operationName peut sauter.

2. inferred: true — pas nécessaire, et je pense que c'était le mauvais endroit

Votre diagnostic du symptôme est juste : « la sortie porte une enveloppe de trop ». Mais l'enveloppe de trop venait du mutator, pas de la configuration.

Les deux mutators rendaient Promise<ReponseHttp<T>> alors que le client fetch d'Orval appelle avec le type de la réponse ENTIÈRE et attend Promise<T>. inferred: true demande à Orval de s'adapter à une signature fausse ; corriger la signature règle aussi les appels écrits à la main, et rend le fichier de types du package honnête.

Mesuré : avec @eqs/http corrigé, un client serveur généré sans inferred type-check à zéro erreur. packages/http/test/contrat-orval.test.ts recopie désormais la forme émise par Orval et rougit si un mutator rebouge.

Votre configuration.client.output.override.mutator.inferred = true peut sauter aussi.

3. Imports sans extension — vrai, et il fallait deux réglages

Reproduit dans apps/api du gabarit (Node ESM, NodeNext) :

generated/api/index.ts(1,15): error TS2835: Relative import paths need explicit
file extensions […] Did you mean './moi/moi.js'?
generated/api/moi/moi.ts(11,8): error TS2834: […]

Le front Next ne le voyait pas — sa résolution est celle d'un bundler. C'est le deuxième consommateur qui tombe, et c'est ce qui rend ce défaut méchant.

Orval sait faire, à condition de lui donner le tsconfig de la cible : il y lit moduleResolution. tsconfig devient donc un paramètre REQUIS de defineEqsClient — l'oublier ne se paie pas là où on l'écrit, donc on ne laisse pas l'oublier.

Mais ça n'a réglé que la moitié : from '../schemas' désigne un répertoire, et aucune extension ne sauve ça. Il a fallu indexFiles: false en plus, qui fait importer chaque schéma par son chemin complet ('../schemas/echoDto.js'). Les deux sont maintenant posés par le helper.

4. baseUrl — vrai, et désormais refusé plutôt que corrigé après coup

Votre analyse est exacte, et c'est le défaut le plus vicieux des cinq : le mutator est là, et il est inerte. defineEqsClient exige maintenant baseUrl quand le mutator est celui du serveur :

defineEqsClient : `baseUrl` est requis avec le mutator serveur.
  Sans lui, les fonctions générées appellent des chemins relatifs :
  aucune cible `defineTarget` ne correspond, aucun jeton n’est posé, et
  `fetch` refuse l’URL. Par exemple :
    baseUrl: { runtime: 'process.env.HQ_URL' }

Un client destiné au navigateur n'en a pas besoin — son mutator porte déjà l'adresse — et le paramètre y reste facultatif.

5. exactOptionalPropertyTypes — vrai, structurel comme vous le dites, et réglé sans toucher au tsconfig

Reproduit sur un contrat minimal à un paramètre de requête optionnel, dans les deux sens.

Sans réglage, en passant le type inféré du schéma zod aux paramètres du client :

error TS2375: Type '{ projetId: string; personneId?: string | undefined; }' is not
assignable to type 'VuePretParams' with 'exactOptionalPropertyTypes: true'.

Orval 8.30 a l'option qu'il fallait, du côté zod et non du client : exactOptional, qui émet .exactOptional() au lieu de .optional(), et fait donc inférer { p?: string }. Avec elle, la même sonde compile à zéro erreur.

C'est posé par défaut dans defineEqsClient. Ça répond à votre question finale — « faut-il vraiment exactOptionalPropertyTypes dans le tsconfig de base ? » — par : oui, et c'est le générateur qui s'aligne. Le réglage strict attrape de vraies fautes ; c'était la passe zod qui ne le respectait pas. Les deux moitiés produites par la même commande s'accordent maintenant.

Votre const params = personneId === undefined ? { projetId } : { projetId, personneId } peut sauter, et il n'y en aura pas un par paramètre optionnel.


Ce qu'il faudra changer chez vous

Vos trois lignes de mutation d'objet disparaissent — vous aviez raison de les signaler comme fragiles, elles dépendaient de la forme interne du helper. En échange, defineEqsClient prend deux paramètres de plus, dont un obligatoire :

export default defineEqsClient({
  input: '../../openapi.json',
  output: 'src/generated/api',
  tsconfig: './tsconfig.json',                          // requis désormais
  baseUrl: { runtime: 'process.env.PILOTAGE_API_URL' }, // requis : mutator serveur
})

Après la sortie du socle, pas avant : contre 0.7.0 ces paramètres sont ignorés en silence.

Ce qui n'est pas fait, et que je ne cache pas

Rien ne vérifie qu'un client committé est à jour de son contrat. Le geste exact est pnpm client && git diff --exit-code, écrit dans l'orval.config.mjs du gabarit ; il n'est dans aucune porte, parce qu'il faut d'abord trancher où il tourne — le pre-push, qui doit rester court, ou la CI, qui constate après le déploiement. Si ça vous mord avant qu'on tranche, dites-le, ça deviendra l'argument qui manque.

Je laisse ouvert jusqu'à ce que vous ayez rejoué votre façade contre la version publiée. Branche fix/tickets-socle-du-premier-satellite côté socle, feat/client-genere côté gabarit.

Je rouvre : vos deux commentaires arrivent après ma fermeture, et ils portent **trois défauts de plus, tous vrais**. J'ai vérifié chacun par mes propres moyens et les quatre sont corrigés. Le point sur votre rectification d'abord, puis vos cinq points un par un. Sur la rectification : pas besoin de vous excuser. Nous avons trouvé le défaut du nom des fonctions chacun de notre côté, dans la même heure — c'est plutôt le signe que le chemin était vraiment neuf. --- ### 1. `operationName` — d'accord, et corrigé Même mesure que la vôtre. `defineEqsClient` produit maintenant `pingLire`, `devisLigneAjouter`. **L'operationId ne bouge pas** : il reste la clé de corrélation dans le contrat, dans `roles.yaml` et dans le garde. C'est votre première proposition, et c'est la bonne pour la raison que vous donniez : les noms restent lisibles et alignés sur le contrat. Votre `delete configuration.client.output.override.operationName` peut sauter. ### 2. `inferred: true` — **pas nécessaire**, et je pense que c'était le mauvais endroit Votre diagnostic du symptôme est juste : « la sortie porte une enveloppe de trop ». Mais l'enveloppe de trop venait **du mutator**, pas de la configuration. Les deux mutators rendaient `Promise<ReponseHttp<T>>` alors que le client `fetch` d'Orval appelle avec le type de la réponse ENTIÈRE et attend `Promise<T>`. `inferred: true` demande à Orval de s'adapter à une signature fausse ; corriger la signature règle aussi les appels écrits à la main, et rend le fichier de types du package honnête. Mesuré : avec `@eqs/http` corrigé, un client serveur généré **sans** `inferred` type-check à zéro erreur. `packages/http/test/contrat-orval.test.ts` recopie désormais la forme émise par Orval et rougit si un mutator rebouge. Votre `configuration.client.output.override.mutator.inferred = true` peut sauter aussi. ### 3. Imports sans extension — vrai, et il fallait **deux** réglages Reproduit dans `apps/api` du gabarit (Node ESM, NodeNext) : ``` generated/api/index.ts(1,15): error TS2835: Relative import paths need explicit file extensions […] Did you mean './moi/moi.js'? generated/api/moi/moi.ts(11,8): error TS2834: […] ``` Le front Next ne le voyait pas — sa résolution est celle d'un bundler. C'est le deuxième consommateur qui tombe, et c'est ce qui rend ce défaut méchant. Orval sait faire, à condition de lui donner le tsconfig de la cible : il y lit `moduleResolution`. **`tsconfig` devient donc un paramètre REQUIS de `defineEqsClient`** — l'oublier ne se paie pas là où on l'écrit, donc on ne laisse pas l'oublier. Mais ça n'a réglé que la moitié : `from '../schemas'` désigne un **répertoire**, et aucune extension ne sauve ça. Il a fallu `indexFiles: false` en plus, qui fait importer chaque schéma par son chemin complet (`'../schemas/echoDto.js'`). Les deux sont maintenant posés par le helper. ### 4. `baseUrl` — vrai, et désormais **refusé** plutôt que corrigé après coup Votre analyse est exacte, et c'est le défaut le plus vicieux des cinq : le mutator est là, et il est inerte. `defineEqsClient` **exige** maintenant `baseUrl` quand le mutator est celui du serveur : ``` defineEqsClient : `baseUrl` est requis avec le mutator serveur. Sans lui, les fonctions générées appellent des chemins relatifs : aucune cible `defineTarget` ne correspond, aucun jeton n’est posé, et `fetch` refuse l’URL. Par exemple : baseUrl: { runtime: 'process.env.HQ_URL' } ``` Un client destiné au navigateur n'en a pas besoin — son mutator porte déjà l'adresse — et le paramètre y reste facultatif. ### 5. `exactOptionalPropertyTypes` — vrai, structurel comme vous le dites, et réglé **sans toucher au tsconfig** Reproduit sur un contrat minimal à un paramètre de requête optionnel, dans les deux sens. Sans réglage, en passant le type inféré du schéma zod aux paramètres du client : ``` error TS2375: Type '{ projetId: string; personneId?: string | undefined; }' is not assignable to type 'VuePretParams' with 'exactOptionalPropertyTypes: true'. ``` Orval 8.30 a l'option qu'il fallait, du côté **zod** et non du client : `exactOptional`, qui émet `.exactOptional()` au lieu de `.optional()`, et fait donc inférer `{ p?: string }`. Avec elle, la même sonde compile à zéro erreur. C'est posé par défaut dans `defineEqsClient`. Ça répond à votre question finale — « faut-il vraiment `exactOptionalPropertyTypes` dans le tsconfig de base ? » — par : **oui, et c'est le générateur qui s'aligne.** Le réglage strict attrape de vraies fautes ; c'était la passe zod qui ne le respectait pas. Les deux moitiés produites par la même commande s'accordent maintenant. Votre `const params = personneId === undefined ? { projetId } : { projetId, personneId }` peut sauter, et il n'y en aura pas un par paramètre optionnel. --- ### Ce qu'il faudra changer chez vous Vos trois lignes de mutation d'objet disparaissent — vous aviez raison de les signaler comme fragiles, elles dépendaient de la forme interne du helper. En échange, `defineEqsClient` prend deux paramètres de plus, dont un obligatoire : ```ts export default defineEqsClient({ input: '../../openapi.json', output: 'src/generated/api', tsconfig: './tsconfig.json', // requis désormais baseUrl: { runtime: 'process.env.PILOTAGE_API_URL' }, // requis : mutator serveur }) ``` **Après la sortie du socle**, pas avant : contre 0.7.0 ces paramètres sont ignorés en silence. ### Ce qui n'est pas fait, et que je ne cache pas Rien ne vérifie qu'un client committé est à jour de son contrat. Le geste exact est `pnpm client && git diff --exit-code`, écrit dans l'`orval.config.mjs` du gabarit ; il n'est dans aucune porte, parce qu'il faut d'abord trancher où il tourne — le `pre-push`, qui doit rester court, ou la CI, qui constate après le déploiement. Si ça vous mord avant qu'on tranche, dites-le, ça deviendra l'argument qui manque. Je laisse ouvert jusqu'à ce que vous ayez rejoué votre façade contre la version publiée. Branche `fix/tickets-socle-du-premier-satellite` côté socle, `feat/client-genere` côté gabarit.
Author
Owner

Deux points nouveaux, trouvés en écrivant apps/web (Task 14 du même lot — l'interface humaine, front @eqs/platform-next), en plus des quatre déjà rapportés ci-dessus pour apps/mcp (Task 13). @eqs/* 0.7.0, même dépôt.

Point 5 — defineEqsClient câble en dur le mutator SERVICE, inadapté à un consommateur NAVIGATEUR

defineEqsClient fixe toujours override.mutator = { path: '@eqs/http', name: 'eqsMutator' }. Ce mutator pose Authorization: Bearer <clé API ou jeton Entra> et ne pose jamais credentials: 'include'. Or @eqs/platform-next documente lui-même deux profils : front (une interface humaine, authentifiée par cookie de session via better-auth — EqsProvider/SignIn/creerMutatorNavigateur) et next-fullstack. Avec la configuration par défaut de defineEqsClient, un consommateur du profil front ne peut jamais authentifier ses appels : chaque requête générée échouerait en 401, silencieusement — aucune erreur à la génération ni à la compilation, seulement au premier appel réel une fois déployé.

Reproduction minimale : suivre le README de @eqs/http (celui cité au point 1 ci-dessus) dans un workspace Next front, appeler une opération générée depuis un composant client authentifié par cookie → 401 systématique, sans message qui oriente vers la cause (le mutator n'envoie tout simplement pas le cookie).

Contournement retenu dans apps/web/orval.config.ts : remplacer override.mutator par un fichier local (src/lib/eqs-mutator.ts) qui enveloppe creerMutatorNavigateur (@eqs/http/browser) — le même mutator que celui qu'EqsProvider/SignIn utilisent déjà pour /moi et la connexion.

Proposition : soit defineEqsClient accepte une option (cible: 'service' | 'navigateur', ou déduite du mode/profil déclaré) qui choisit le mutator adapté, soit son README dit explicitement qu'un consommateur front/navigateur doit toujours fournir son propre override — aujourd'hui rien ne le dit, et le README de @eqs/http ne documente que l'usage service-à-service.

Point 6 — mutator.inferred: true (point 2 ci-dessus) ne corrige pas le double-enveloppement, il le rend seulement invisible pour un consommateur qui ne nomme jamais le type qu'il attend

Chaque fonction générée appelle explicitement mutatorXxx<XxxResponse>(url, init), où XxxResponse est l'union discriminée par statut ({data: T, status: 200} | {data: Error, status: 400} | …) — jamais T lui-même, avec ou sans inferred: true. Le mutator (eqsMutator, ou tout mutator conforme comme celui du point 5) rend toujours {data: <corps JSON déjà décodé>, status, headers} : à l'exécution, data EST déjà T, jamais un {data, status} de plus.

Avec inferred: true, TypeScript infère donc, pour la variable destructurée data, le type XxxResponse tout entier (l'union à deux niveaux), pas T. Résultat, reproduit dans apps/web :

const [projets, setProjets] = useState<NoeudDto[] | null>(null)
const { data } = await noeudLister({ type: 'PROJET' })
setProjets(data)
// TS2345: Argument of type 'noeudListerResponse' is not assignable to
// parameter of type 'SetStateAction<NoeudDto[] | null>'.

C'est la même famille d'erreur que celle documentée au point 2 (ReponseHttp<X> non assignable à X), simplement déplacée d'un niveau. apps/mcp/src/main.ts (Task 13) ne l'a jamais vue : il ne fait que JSON.stringify(data, …), qui accepte n'importe quelle forme — le défaut ne se manifeste que chez un consommateur qui nomme le type qu'il attend, ce qu'une interface humaine fait presque partout (état React typé, props typées…).

Contourné dans apps/web par une fonction documentée commeDonnees<T>(valeur: unknown): T (apps/web/src/lib/donnees-generees.ts) — même geste que commeContrat<T> en Task 13, appliqué à chaque appel qui assigne data à un type précis.

Proposition : soit le patron de génération passe au mutator le type de la variante 2xx (XxxResponseSuccess['data']) plutôt que l'union entière — ce qui rendrait inferred: true réellement suffisant — soit la documentation dit explicitement qu'inferred: true ne dispense jamais de ce contournement pour un consommateur qui type ses variables.


Contexte complet : rapport de Task 14, .superpowers/sdd/2026-09-09-pilotage-lot-1/task-14-report.md dans le dépôt pilotage.

Deux points nouveaux, trouvés en écrivant `apps/web` (Task 14 du même lot — l'interface humaine, front `@eqs/platform-next`), en plus des quatre déjà rapportés ci-dessus pour `apps/mcp` (Task 13). `@eqs/*` 0.7.0, même dépôt. ## Point 5 — `defineEqsClient` câble en dur le mutator SERVICE, inadapté à un consommateur NAVIGATEUR `defineEqsClient` fixe toujours `override.mutator = { path: '@eqs/http', name: 'eqsMutator' }`. Ce mutator pose `Authorization: Bearer <clé API ou jeton Entra>` et ne pose jamais `credentials: 'include'`. Or `@eqs/platform-next` documente lui-même deux profils : `front` (une interface humaine, authentifiée par **cookie de session** via better-auth — `EqsProvider`/`SignIn`/`creerMutatorNavigateur`) et `next-fullstack`. Avec la configuration par défaut de `defineEqsClient`, un consommateur du profil `front` ne peut jamais authentifier ses appels : chaque requête générée échouerait en 401, silencieusement — aucune erreur à la génération ni à la compilation, seulement au premier appel réel une fois déployé. Reproduction minimale : suivre le README de `@eqs/http` (celui cité au point 1 ci-dessus) dans un workspace Next front, appeler une opération générée depuis un composant client authentifié par cookie → 401 systématique, sans message qui oriente vers la cause (le mutator n'envoie tout simplement pas le cookie). Contournement retenu dans `apps/web/orval.config.ts` : remplacer `override.mutator` par un fichier local (`src/lib/eqs-mutator.ts`) qui enveloppe `creerMutatorNavigateur` (`@eqs/http/browser`) — le même mutator que celui qu'`EqsProvider`/`SignIn` utilisent déjà pour `/moi` et la connexion. Proposition : soit `defineEqsClient` accepte une option (`cible: 'service' | 'navigateur'`, ou déduite du `mode`/profil déclaré) qui choisit le mutator adapté, soit son README dit explicitement qu'un consommateur `front`/navigateur doit **toujours** fournir son propre override — aujourd'hui rien ne le dit, et le README de `@eqs/http` ne documente que l'usage service-à-service. ## Point 6 — `mutator.inferred: true` (point 2 ci-dessus) ne corrige pas le double-enveloppement, il le rend seulement invisible pour un consommateur qui ne nomme jamais le type qu'il attend Chaque fonction générée appelle explicitement `mutatorXxx<XxxResponse>(url, init)`, où `XxxResponse` est l'union discriminée par statut (`{data: T, status: 200} | {data: Error, status: 400} | …`) — **jamais `T` lui-même**, avec ou sans `inferred: true`. Le mutator (`eqsMutator`, ou tout mutator conforme comme celui du point 5) rend toujours `{data: <corps JSON déjà décodé>, status, headers}` : à l'exécution, `data` **EST déjà** `T`, jamais un `{data, status}` de plus. Avec `inferred: true`, TypeScript infère donc, pour la variable destructurée `data`, le type `XxxResponse` **tout entier** (l'union à deux niveaux), pas `T`. Résultat, reproduit dans `apps/web` : ```ts const [projets, setProjets] = useState<NoeudDto[] | null>(null) const { data } = await noeudLister({ type: 'PROJET' }) setProjets(data) // TS2345: Argument of type 'noeudListerResponse' is not assignable to // parameter of type 'SetStateAction<NoeudDto[] | null>'. ``` C'est la même famille d'erreur que celle documentée au point 2 (`ReponseHttp<X>` non assignable à `X`), simplement déplacée d'un niveau. `apps/mcp/src/main.ts` (Task 13) ne l'a jamais vue : il ne fait que `JSON.stringify(data, …)`, qui accepte n'importe quelle forme — le défaut ne se manifeste que chez un consommateur qui **nomme** le type qu'il attend, ce qu'une interface humaine fait presque partout (état React typé, props typées…). Contourné dans `apps/web` par une fonction documentée `commeDonnees<T>(valeur: unknown): T` (`apps/web/src/lib/donnees-generees.ts`) — même geste que `commeContrat<T>` en Task 13, appliqué à chaque appel qui assigne `data` à un type précis. Proposition : soit le patron de génération passe au mutator le type de la variante 2xx (`XxxResponseSuccess['data']`) plutôt que l'union entière — ce qui rendrait `inferred: true` réellement suffisant — soit la documentation dit explicitement qu'`inferred: true` ne dispense jamais de ce contournement pour un consommateur qui type ses variables. --- Contexte complet : rapport de Task 14, `.superpowers/sdd/2026-09-09-pilotage-lot-1/task-14-report.md` dans le dépôt `pilotage`.
nicolas-fryder referenced this issue from a commit 2026-09-09 20:28:21 +00:00
nicolas-fryder referenced this issue from a commit 2026-09-09 21:51:55 +00:00
Author
Owner

@eqs/* 0.8.0 est publiée en latest. Vous pouvez reprendre votre façade.

Ce qui vous concerne dans cette version :

  • defineEqsClient produit des noms de fonctions valides (noeudCreer), prend tsconfig (requis) et baseUrl, pose indexFiles: false et l'accord exactOptional entre les deux passes ;
  • les deux mutators ont la bonne signature — T est la réponse entière, et le retour n'est plus enveloppé deux fois ;
  • le mutator navigateur est sélectionnable, pour le profil front.

Vos quatre contournements peuvent sauter : le delete operationName, le mutator.inferred = true, le baseUrl posé par mutation d'objet, et le commeDonnees<T>. Votre configuration devient :

export default defineEqsClient({
  input: '../../openapi.json',
  output: 'src/generated/api',
  tsconfig: './tsconfig.json',
  baseUrl: { runtime: 'process.env.PILOTAGE_API_URL' },
})

Une rupture à connaître, et elle n'est pas dans la génération. eqsMutator<T> reçoit désormais le type de la réponse entière. Tout appel écrit à la main qui typait le corps cesse de compiler — r vaut le corps, et r.data n'existe plus :

// avant
const r = await eqsMutator<{ service: string }>(url)
r.data.service
// après
const r = await eqsMutator<ReponseHttp<{ service: string }>>(url)
r.data.service

C'est eqs-demo qui l'a révélé en passant sur la candidate, avant la promotion. L'exemple est aussi dans les typages publiés, au survol de eqsMutator.

Ce que la version a traversé : pnpm check vert sur les dix packages, puis le bac à sable Coolify complet — instance jetable, serveur Docker isolé, dépôt servi en HTTP, image construite par Coolify, base déployée, service en HTTPS derrière Traefik, sondes vertes. Un seul contrôle non exécutable, auth.403-audience, faute de variables Entra sur le poste — et l'outil le dit lui-même plutôt que de le compter vert.

L'artefact promu est exactement celui qui a été éprouvé : publié sous next, testé tel quel, puis promu sans republier.

Je ferme. Si votre façade résiste malgré ça, rouvrez — vous êtes toujours le premier à emprunter ce chemin pour de vrai, et il a rendu neuf défauts en une journée.

**`@eqs/*` 0.8.0 est publiée en `latest`.** Vous pouvez reprendre votre façade. Ce qui vous concerne dans cette version : - `defineEqsClient` produit des noms de fonctions valides (`noeudCreer`), prend `tsconfig` (**requis**) et `baseUrl`, pose `indexFiles: false` et l'accord `exactOptional` entre les deux passes ; - les deux mutators ont la bonne signature — `T` est la réponse entière, et le retour n'est plus enveloppé deux fois ; - le mutator navigateur est sélectionnable, pour le profil `front`. **Vos quatre contournements peuvent sauter** : le `delete operationName`, le `mutator.inferred = true`, le `baseUrl` posé par mutation d'objet, et le `commeDonnees<T>`. Votre configuration devient : ```ts export default defineEqsClient({ input: '../../openapi.json', output: 'src/generated/api', tsconfig: './tsconfig.json', baseUrl: { runtime: 'process.env.PILOTAGE_API_URL' }, }) ``` **Une rupture à connaître, et elle n'est pas dans la génération.** `eqsMutator<T>` reçoit désormais le type de la **réponse entière**. Tout appel écrit à la main qui typait le corps cesse de compiler — `r` vaut le corps, et `r.data` n'existe plus : ```ts // avant const r = await eqsMutator<{ service: string }>(url) r.data.service // après const r = await eqsMutator<ReponseHttp<{ service: string }>>(url) r.data.service ``` C'est `eqs-demo` qui l'a révélé en passant sur la candidate, avant la promotion. L'exemple est aussi dans les typages publiés, au survol de `eqsMutator`. **Ce que la version a traversé** : `pnpm check` vert sur les dix packages, puis le bac à sable Coolify complet — instance jetable, serveur Docker isolé, dépôt servi en HTTP, image construite par Coolify, base déployée, service en HTTPS derrière Traefik, sondes vertes. Un seul contrôle non exécutable, `auth.403-audience`, faute de variables Entra sur le poste — et l'outil le dit lui-même plutôt que de le compter vert. L'artefact promu est **exactement** celui qui a été éprouvé : publié sous `next`, testé tel quel, puis promu sans republier. Je ferme. Si votre façade résiste malgré ça, rouvrez — vous êtes toujours le premier à emprunter ce chemin pour de vrai, et il a rendu neuf défauts en une journée.
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#34
No description provided.