diff --git a/ROADMAP.md b/ROADMAP.md index 59a9c48..e4d5422 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,6 +17,17 @@ contient que ce qui reste à faire. Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`. +### L4.0 — La resource `api://catalog` enseigne le piège D3 + +Constaté le 25/08/2026 en préparant la passation : `src/resources/apis.js` +documente un exemple `QueryExecute` avec `"QueryType": 1` (exactement ce que D3 +interdit de recopier), un `.Select(z => z)` dans l'expression (contraire à la +répartition expression/options, et D13), et l'entité fantôme `Aliases` +(corrigée partout ailleurs au lot 2). Cette resource est lue par les sessions +Claude : c'est une source de désinformation active. Corriger l'exemple +(`QueryType: 0`, expression sans `Select`), `Aliases` → `Alias`, et renvoyer +vers `get_entity_metadata` comme source de vérité. + ### L4.1 — Le modèle Writing est inatteignable `QueryType` est figé à `0` (Reading) en dur dans `api-service.js` diff --git a/docs/handoff-lot4.md b/docs/handoff-lot4.md new file mode 100644 index 0000000..26251ee --- /dev/null +++ b/docs/handoff-lot4.md @@ -0,0 +1,240 @@ +# Passation — lot 4 (L4.0, L4.1, L4.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** : trois blocs de [../ROADMAP.md](../ROADMAP.md) — +corriger la resource `api://catalog` qui enseigne le piège D3 (L4.0), +exposer `query_type` sur les outils de requête (L4.1), et rendre les +applications autres qu'`EasyWMS` accessibles via un paramètre `application` +sur les outils AD et workflow (L4.2). + +**Hors périmètre** : +- **L4.3** (identifier le MCP dans les logs) : dépend d'une configuration côté + WMS, hors de portée d'une session de codage. +- **L4.4** (API WorkflowLog) et l'exploration du contexte **Metrics** : ce sont + des investigations, elles feront l'objet d'une phase séparée dont le livrable + sera un rapport, pas du code. N'y touche pas. +- **L4.5** (`Parameters`, `QueryExecuteStream`…) : consigné, pas ce lot. +- **Aucun nouvel outil** : le compte reste à 23. Les deux correctifs sont des + paramètres sur des outils existants. +- 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` (par défaut), host `10.255.255.2`, tenant + `LIMAGRAI2512`. +- **Le profil `AD` est cassé et c'est diagnostiqué — ne le réinvestigue pas** + (tenant introuvable côté STS, point ouvert de la ROADMAP). `npm test -- --all` + échoue 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 +``` + +Sonder le WMS directement (court-circuite les outils) : + +```bash +node -e " +require('dotenv').config(); +const pm=require('./src/config/profile-manager'); pm.loadProfiles(); +const api=require('./src/services/api-service').getInstance(); +(async()=>{ /* … */ })();" +``` + +## 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. +4. Aucun accès base de données (D1). +5. **D23** : le wrapper valide les **noms** de paramètres et les **requis** + contre les schémas — déclare `query_type` et `application` dans les + `inputSchema`, sinon le wrapper les rejettera. Le wrapper ne valide **pas + les valeurs** : les gardes de valeur (`query_type` hors 0-3, etc.) vivent + dans le code de l'outil. +6. **D8** : tout état lié au tenant s'invalide par `profileManager.onSwitch()`, + jamais à la main depuis un autre module. Les caches modifiés en L4.2 + restent abonnés. +7. **D24** : toute sortie potentiellement volumineuse est bornée et signalée + (`truncated`/`hint`). Les pages de workflows CustomApp (153) tiennent + largement ; ne l'oublie pas si tu exposes des listes plus larges. + +**Numéros de décision réservés** : **D25** = exposition de `query_type` (son +rapport à D3), **D26** = paramètre `application` et clés de cache. Vérifie que +D24 est bien la dernière décision avant d'écrire. + +--- + +## Phase 0 — Confirmer les mesures + +Toutes rejouées le 25/08/2026 (LIMAGRAI2512) par la sonde directe. À +**confirmer**, pas à réinvestiguer : + +| Sonde | Constaté le 25/08 | +|---|---| +| `POST /QueryExecute` `{Application:'EasyWMS', QueryType:1, Expression:'Context.Products.OrderBy(z => z.Id)', Take:1}` | **OK, 1 ligne** — le modèle Writing répond | +| Même appel avec `QueryType:3` | HTTP 500 : `'ApplicationMetricDataContext' ne contient pas de définition pour 'Products'` — le contexte Metrics existe, son modèle est autre (ne l'explore pas, hors périmètre) | +| `QueryType:2` | non configuré sur ce tenant (`Could not resolve serviceType 'IDataWarehouse…'`, mesure du 24/08) | +| `POST /Workflow/GetByApplication` payload `['CustomApp', tenant, 5, 0]` (API AD) | **5 workflows** sous la clé **`entities`** de la réponse : `CST_SendRejectContainersToPK`, `Helper_ContainerByCode`, … | + +Points déjà tranchés, **ne les réinvestigue pas** (mesures des 24-25/08 dans la +ROADMAP) : +- Le champ `Application` de `QueryExecute` **ne partitionne rien** (contexte + commun au tenant) : inutile de le paramétrer côté requêtes. +- Les 11 entités `CustomApp` ne sont requêtables dans **aucun** contexte — + l'API AD est le seul accès au spécifique client. +- `ClientModule` est sans effet (L4.3). + +--- + +## L4.0 — Corriger la resource `api://catalog` + +**Problème.** `src/resources/apis.js` (resource lue par les sessions Claude) +documente un exemple `QueryExecute` avec `"QueryType": 1` — exactement ce que +D3 interdit de recopier —, un `.Select(z => z)` dans l'expression (contraire à +la répartition expression/options), et l'entité fantôme `Aliases` (corrigée +partout ailleurs au lot 2). + +**À faire.** Exemple avec `QueryType: 0` et expression sans `Select`, +`Aliases` → `Alias`, renvoi vers `get_entity_metadata` comme source de vérité +sur les entités. Relis toute la resource pendant que tu y es — signale (sans +forcément corriger) toute autre affirmation contredite par DECISIONS.md. + +**Vérification attendue** : lire `api://catalog` via `resources/read` en +protocole ; la sortie ne contient plus ni `"QueryType": 1` ni `Aliases`. + +## L4.1 — Exposer `query_type` sur les outils de requête + +**Problème.** `QueryType` est figé à `0` en dur à deux endroits : +`src/services/api-service.js:333` (`executeQuery`) et `:370` +(`executeScalarQuery`). Le modèle Writing — opérationnel, mesuré — est +inatteignable. + +**À faire.** +- Paramètre `query_type` (entier, défaut `0`) sur **trois outils** : + `call_query_api`, `query_wms_entities`, `count_wms_entities`. Pas sur + `get_entity_schema` ni `search_wms_data` — ils restent des raccourcis + Reading. +- Garde de valeur **dans le code** (le wrapper D23 ne valide pas les + valeurs) : hors `0..3` → erreur locale actionnable avant tout réseau, + nommant les quatre contextes. `2` et `3` sont transmis tels quels : le WMS + répond, et depuis L1.1 son diagnostic remonte entier. +- **D3 reste la règle par défaut** : en Writing les statuts sont des + énumérations, `== "Release"` échoue. La bascule est un opt-in explicite — + les descriptions d'outils doivent porter l'avertissement. Acte le rapport + D3/`query_type` en **D25**. +- **Interaction avec le resolver (attention, c'est le point délicat)** : la + table de résolution est construite sur le Metadata **Reading**. Quand + `query_type != 0` : un nom qui se résout se résout normalement (`Products` + marche en Writing, mesuré) ; un nom **inconnu** du Reading ne doit **pas** + être bloqué en dur — passe-le tel quel avec un `warning` dans la réponse + (même mécanique que le repli « Metadata injoignable » existant), car le + modèle Writing/Metrics peut contenir des entités hors Reading. + +**Vérification attendue** (protocole, LIMAGRAIN) : +- `call_query_api("Products", query_type: 1, limit: 1)` → succès, 1 ligne. +- `call_query_api("Products", query_type: 3)` → erreur structurée contenant + `ApplicationMetricDataContext`. +- `call_query_api("Products", query_type: 7)` → erreur locale avant réseau + (aucun `[API] POST` dans stderr), nommant les valeurs valides. +- `query_wms_entities("Container", limit: 1)` sans `query_type` → strictement + le comportement d'aujourd'hui (résolu `Containers`, Reading). +- Un nom inconnu avec `query_type: 1` → transmis tel quel avec `warning`, pas + d'échec local. +- `count_wms_entities("Product", query_type: 1)` → un nombre (~51 160). + +## L4.2 — Paramètre `application` sur les outils AD et workflow + +**Problème.** `application` vient du profil (`WMS_APPLICATION`, partagé) : +`src/services/ad-service.js:85` et `src/services/workflow-service.js:50`. Le +MCP n'interroge donc jamais que `EasyWMS`. Or **CustomApp porte le spécifique +client** (153 workflows `CST_*` sur ce tenant) — précisément ce qu'on cherche +en debug — et 9 applications sont déclarées (`POST /AD/api/Application/GetAll`, +payload `null`). + +**À faire.** +- Paramètre `application` (défaut : l'application du profil, donc comportement + inchangé sans lui) sur : `get_ad_elements`, `search_ad_elements`, + `get_ad_element_details`, `search_workflows`, `get_workflow_details`, + `list_workflow_categories`. +- **Clés de cache** : `ad-service` passe de « un cache par type » à « un cache + par (application, type) » ; `workflow-service` de « un cache global » à « un + cache par application ». Sans ça, un appel CustomApp pollue le cache EasyWMS. + L'abonnement `onSwitch()` continue d'invalider **tout** (D8). Acte le contrat + en **D26**. +- `get_application_summary` doit refléter les nouvelles clés (état par + application et par type) sans exploser en volume (D24). +- `list_workflow_categories` : adosse la liste à `Application/GetAll` (9 + applications, comptes réels par application) plutôt qu'aux `applicationName` + du seul cache EasyWMS. La note de L1.3 reste vraie — il n'existe pas de champ + catégorie ; la réponse liste des applications et le dit. Le paramètre + `category` de `search_workflows` (filtre sur `applicationName`) doit rester + cohérent avec le nouveau paramètre `application` — documente leur + articulation dans les descriptions, ne casse ni l'un ni l'autre. +- **Pente naturelle interdite** : ne précharge pas les 9 applications (le type + `Resource` pèse 29 374 éléments sur la seule EasyWMS). Le chargement reste + paresseux, par application effectivement demandée. + +**Vérification attendue** (protocole, LIMAGRAIN) : +- `search_workflows("CST_", application: "CustomApp")` → objets peuplés + (`CST_SendRejectContainersToPK`…). +- `get_ad_elements("Workflow", application: "CustomApp")` → éléments `CST_*`. +- Séquence **séquentielle** EasyWMS → CustomApp → EasyWMS sur + `search_workflows` : les comptes restent distincts (~4 012 vs 153), aucune + pollution croisée ; `get_application_summary` montre les deux caches. +- Sans paramètre `application` → comportement strictement inchangé. +- `switch_wms_profile` puis retour → caches invalidés (log `Cache cleared`). + +--- + +## Méthode + +1. Phase 0 d'abord (sondes rejouées telles quelles — la forme `entities` de la + réponse AD est déjà établie, ne la redécouvre pas). +2. L4.0, puis L4.1, puis L4.2 — chaque bloc vérifié **en exécution via le + protocole** avant de passer au suivant. +3. **Attention à la concurrence** : le serveur traite les `tools/call` en + concurrence (point ouvert de la ROADMAP). Pour les vérifications qui + enchaînent bascules ou séquences de cache, envoie les requêtes + **séquentiellement** (attendre chaque réponse), pas en rafale. +4. Les schémas changent : reboucler sur les 23 noms de `tools/list` (aucun + `Unknown tool` ; `execute_command` vérifié statiquement, non appelé) et + vérifier qu'un paramètre inconnu est toujours rejeté (D23). +5. Baseline avant/après : handshake (23/6) + `npm test` (4/4, exit 0). +6. « 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 bloc (L4.0, L4.1, L4.2), messages expliquant le pourquoi, + **mesures rejouées dans le corps du message**. +- Documentation dans les mêmes commits : **D25** et **D26** dans DECISIONS.md ; + CLAUDE.md — nuancer « `QueryType: 0`, jamais 1 » en « défaut 0, `query_type` + est un opt-in documenté (D25) », documenter le paramètre `application` et les + nouvelles clés de cache dans la section Caches ; ROADMAP.md — retirer L4.0, + L4.1, L4.2 (L4.3, L4.4, L4.5 restent). +- 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 bloc, la vérification attendue rejouée et + son résultat **mesuré** (colle les sorties), plus la baseline finale. + Laisse `handoff-lot4.md` en place pour la révision.