Supprime handoff-lot2.md : passation livrée et révisée

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-25 09:35:46 +02:00
parent 37c68a4d9a
commit fdebca500f
-213
View File
@@ -1,213 +0,0 @@
# Passation — lot 2
Tu travailles sur `mcp-wms-api` : un serveur MCP (Node.js, CommonJS, stdio) qui
donne à Claude un accès en lecture à un WMS EasyWMS (Mecalux) via ses API REST.
Lis [../CLAUDE.md](../CLAUDE.md) et [../DECISIONS.md](../DECISIONS.md) avant de
toucher au code.
**Mission** : livrer le lot 2 de [../ROADMAP.md](../ROADMAP.md) — résolution
des noms d'entités via l'API Metadata (L2.1), rejet des paramètres inconnus
(L2.2), deux garde-fous d'arguments manquants (L2.3) — plus un petit correctif
du lot 3 (L3.2, erreurs d'authentification muettes).
**Hors périmètre** : tout le reste de la roadmap (L3.1, lot 4), le build pkg,
le déploiement. Ne pousse rien (`git push` interdit), ne touche pas au `.env`,
n'appelle jamais `execute_command` (il écrit dans le WMS).
---
## Contexte matériel
- Profil de travail : `LIMAGRAIN` (profil par défaut), host `10.255.255.2`,
tenant `LIMAGRAI2512`. `EUROTRAFIC` fonctionne aussi (SaaS).
- **Le profil `AD` est cassé et c'est déjà diagnostiqué — ne le réinvestigue
pas.** Son tenant n'existe plus côté STS (`400 Tenant not found`). Conséquence
pratique : `npm test -- --all` échouera toujours sur AD ; la baseline se
mesure avec `npm test` (profil par défaut), attendu **4/4, code de sortie 0**.
- Baseline protocolaire à préserver : **23 outils**, **6 resources**, aucune
écriture sur stdout hors JSON-RPC.
Handshake + comptages :
```bash
printf '%s\n%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node src/index.js 2>/dev/null | node -e "let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{for(const l of b.split('\n').filter(Boolean)){const m=JSON.parse(l);if(m.id===2)console.log('tools:',m.result.tools.length);if(m.id===3)console.log('resources:',m.result.resources.length);}});"
```
Appeler un outil via le protocole (la seule preuve qu'un outil marche) :
```bash
printf '%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"NOM","arguments":{}}}' | node src/index.js 2>/dev/null
```
## Contraintes non négociables
1. `console.error()` uniquement — une écriture sur stdout casse Claude Desktop
(D6).
2. Un outil ne plante jamais le serveur : erreurs en réponse structurée
`{ success: false, error, tool }`, `isError: true` (wrapper de
`src/index.js`).
3. Messages d'erreur actionnables : dire quoi faire ensuite (convention 4 de
CLAUDE.md).
4. Aucun accès base de données (D1).
5. Tout nouvel état lié au tenant s'invalide par abonnement
`profileManager.onSwitch()`, jamais à la main depuis un autre module (D8).
6. `QueryType: 0` (D3), pas de date relative (D12), 1000 lignes max.
**Numéros de décision réservés** : **D21** = règle `TableName` (L2.1),
**D23** = validation des paramètres (L2.2). N'en attribue pas d'autres sans les
réserver dans DECISIONS.md.
---
## Phase 0 — Vérifications avant de coder
Mesures déjà faites (24/08/2026, `LIMAGRAI2512`) à **confirmer**, pas à
réinvestiguer :
- `GET /Metadata/Entities` renvoie **232 entités** pour `EasyWMS`, chacune avec
`Name` et `TableName` ; `TableName` est **unique** sur les 232.
- Le mapping n'est **pas** une pluralisation : `Container` -> `Containers`,
mais `Alias` -> `Alias` (invariant), et `Item` n'existe pas.
- Le contexte de lecture est **commun au tenant** : la table de résolution doit
agréger le Metadata de **toutes** les applications
(`GET /Metadata/Entities?applicationName=…` — 9 applications, listées par
`POST /AD/api/Application/GetAll`, payload `null`). `CustomApp` renvoie
**0 entité** : c'est mesuré et normal, ne cherche pas pourquoi.
**V1 — le SDK MCP valide-t-il les schémas ?** Mesure indispensable avant L2.2 :
ajoute `additionalProperties: false` à **un** schéma, appelle l'outil via le
protocole avec un paramètre inconnu, et observe. Si le SDK rejette : L2.2 =
ajout sur les 23 schémas. Si le SDK laisse passer (probable) : la validation
doit se faire dans le wrapper `tools/call` de `src/index.js` (à la main ou via
une lib déjà présente dans l'arbre des dépendances — n'ajoute pas de dépendance
lourde sans nécessité). Time-box : 30 min ; si le comportement du SDK est
ambigu, note-le dans le commit et implémente la validation côté wrapper.
---
## L2.1 — Résolution des entités via l'API Metadata
**Problème.** `entity_type` est interpolé sans validation dans
`Context.{entity_type}` à deux endroits : `src/tools/api-tools.js:89` et
`src/services/wms-query-service.js:29` (et `:149`). Le nom attendu est le
`TableName` du Metadata, pas le nom d'entité de l'AD. Un mauvais nom part en
HTTP 500 côté WMS.
**Preuve.** `query_wms_entities("Container")` répond aujourd'hui :
```
Query failed for Container: POST …/QueryExecute failed (HTTP 500): …
Response body: … error CS1061: 'ApplicationReadingContext' ne contient pas de
définition pour 'Container' …
```
**À faire.**
- Une table de résolution `Name|TableName (insensible à la casse) -> TableName`,
construite depuis le Metadata **agrégé sur les 9 applications**, dans un
service (pas dupliquée dans les deux points d'interpolation).
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
**abonnement `onSwitch()`** pour l'invalidation (D8).
- Nom inconnu → échec **avant tout appel réseau**, message actionnable avec
suggestions proches et renvoi vers `get_entity_metadata`, par exemple :
« "Item" n'existe pas dans le modèle Reading. Proches : ItemGroup, StockItem.
232 entités disponibles — utilisez `get_entity_metadata` pour la liste. »
- Si le Metadata est injoignable au moment de résoudre : laisse passer le nom
tel quel (comportement actuel) plutôt que de bloquer tout — et dis-le dans la
réponse.
**Pente naturelle interdite** : ne réinvente pas une règle de pluralisation
(« ajouter un s sauf si… »). Seul `TableName` fait foi ; c'est un mapping mesuré,
pas une grammaire.
**Vérification attendue** (via le protocole, profil LIMAGRAIN) :
- `query_wms_entities("Container", limit 1)`**succès**, 1 ligne (résolu en
`Containers`).
- `query_wms_entities("Alias")` → succès (invariant, pas de pluriel inventé).
- `query_wms_entities("Item")` → échec **sans appel réseau** (aucune ligne
`[API] POST` dans les logs stderr), message avec suggestions.
- `count_wms_entities("Product")` → succès, ~51 160.
- `get_entity_schema` et `call_query_api` bénéficient de la même résolution.
## L2.2 — Rejeter les paramètres inconnus
**Problème.** Le SDK ignore silencieusement les paramètres non déclarés :
`read_recent_logs(lines: 60)` retombe sur `count = 100` sans signal.
**À faire.** Selon le résultat de V1 : `additionalProperties: false` sur les
23 schémas, et/ou validation dans le wrapper `tools/call`. Le message de rejet
nomme le paramètre inconnu **et** les paramètres valides de l'outil.
**Décision déjà tranchée, ne pas rouvrir** : on ne renomme aucun paramètre
(cf. ROADMAP « Écarté »).
**Vérification attendue** : `read_recent_logs` avec `{"lines": 5}` → erreur
structurée nommant `lines` comme inconnu et `count` comme valide. Un appel
valide (`{"count": 5}`) → succès, 5 lignes.
## L2.3 — Garde-fous d'arguments manquants
**Preuves** (mesurées le 24/08/2026, préexistantes au lot 1) :
- `get_workflow_details` avec `{}``success: true` avec **le premier workflow
du cache**. Cause dans `workflow-service.js` `getWorkflowDetails()` : la
recherche compare `w.Id`, `w.Code`, `w.Name` — clés qui n'existent pas sur
les objets réels (minuscules, D5) — donc `undefined === undefined` matche.
- `search_logs` avec `{}`
`Search failed: Cannot read properties of undefined (reading 'toLowerCase')`.
**À faire.** Supprimer les clés mortes (`Id`, `Code`, `Name` majuscules) de la
comparaison de `getWorkflowDetails()` ; rejeter `workflow_id` absent avec un
message actionnable. Garde d'entrée sur `search_logs` nommant le paramètre
attendu. (Si L2.2 rend certains cas impossibles via `required`, garde quand
même la garde côté code : la validation SDK n'est pas garantie.)
**Vérification attendue** : les deux appels avec `{}` renvoient une erreur
structurée actionnable ; `get_workflow_details` avec un id réel renvoie
toujours l'objet brut complet.
## L3.2 — Erreurs d'authentification muettes
**Preuve.** `authenticate()` dans `src/services/api-service.js` (le `catch`
vers la ligne 79) ré-enveloppe l'erreur axios en
`Authentication failed: Request failed with status code 400` et **jette le
corps de la réponse du STS**, qui contenait le diagnostic exact :
`{"error":"invalid_request","error_description":"Tenant not found"}` (mesuré
sur le profil `AD`).
**À faire.** Faire remonter statut + corps de réponse du STS dans le message,
sur le modèle de ce que L1.1 a fait pour `post()`/`get()` (réutilise le même
helper d'enrichissement si possible). **Jamais** les credentials ni les headers
dans le message. Même traitement pour `refreshOAuthToken()`.
**Vérification attendue** : `switch_wms_profile("AD")` puis un
`count_wms_entities("Products")` → la réponse d'outil contient
`Tenant not found` (et aucun mot de passe). Ne « répare » pas le profil AD :
son échec est précisément ce qui rend le correctif vérifiable.
---
## Méthode
1. Phase 0 (V1) d'abord — L2.2 en dépend.
2. Ordre : L2.1, puis L2.2, puis L2.3, puis L3.2. Chaque correctif vérifié **en
exécution via le protocole** avant de passer au suivant.
3. Après tout changement de `src/index.js` ou des schémas : reboucler sur les
23 noms de `tools/list` et vérifier qu'aucun ne répond `Unknown tool`
(n'appelle pas `execute_command` — vérifie sa branche statiquement).
4. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0).
5. « Non résolu » est une réponse acceptable pour une investigation time-boxée ;
une hypothèse présentée comme solution ne l'est pas.
## Livraison
- Un commit par correctif (L2.1, L2.2, L2.3, L3.2), messages expliquant le
pourquoi, mesures dans le corps du message.
- Documentation dans les mêmes commits : **D21** et **D23** dans DECISIONS.md ;
CLAUDE.md — retirer le piège `entity_type` des « Points ouverts » et corriger
la liste d'entités (`Aliases` -> `Alias`, renvoi vers `get_entity_metadata`) ;
ROADMAP.md — retirer L2.1/L2.2/L2.3/L3.2 livrés (L3.3 documentation est
partiellement couverte par ces mises à jour : ajuste-la, ne la supprime que si
tout est fait, l'exemple `wms://query-examples` compris).
- Ne pousse pas. `.env` intact. Toute anomalie hors périmètre découverte en
route : dans ROADMAP.md, pas dans le code.
- Compte-rendu final : pour chaque correctif, la vérification attendue rejouée
et son résultat **mesuré** (colle la sortie), plus la baseline finale.