From 1851b38c623f94c3513de8fa3eb0f26777da82d1 Mon Sep 17 00:00:00 2001 From: Arthur Ria Date: Tue, 25 Aug 2026 15:27:35 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A9vision=20lot=205=20:=20valid=C3=A9=20;?= =?UTF-8?q?=20course=20de=20chargement=20paresseux=20consign=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les vérifications des quatre blocs rejouées via le protocole passent toutes : fenêtre verbatim (23 117 chars par défaut, tranches 20000+20000+20000+11512 = 71 512 reconstituant le blob à l'octet près), plafond de volume (24 432 / 22 992 / 738 sur les trois outils, réponses sous plafond identiques), champ tool présent sur les 15 enveloppes locales (grep 15/15), découvrabilité (hint nommant CustomApp sur résultat vide, aucun hint parasite). Baseline 23/6, npm test 4/4 exit 0. Découverte de révision : sans bascule de profil, une rafale concurrente pendant le chargement paresseux du cache workflow renvoie des résultats faux en silence (count 0 sur CustomApp contre 44 en séquentiel) — les fetchs en vol ne sont pas dédupliqués. Ajouté au point ouvert concurrence avec la piste de correction. handoff-lot5.md supprimé (livré et révisé). Co-Authored-By: Claude Fable 5 --- ROADMAP.md | 10 +- docs/handoff-lot5.md | 242 ------------------------------------------- 2 files changed, 9 insertions(+), 243 deletions(-) delete mode 100644 docs/handoff-lot5.md diff --git a/ROADMAP.md b/ROADMAP.md index b8202e2..47a4be2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -113,7 +113,15 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et pour un usage conversationnel séquentiel, mais Claude peut émettre des appels d'outils **en parallèle** : à traiter si un cas réel de mélange de profils est observé (piste : sérialiser les `tools/call` ou figer le profil résolu au - début de chaque appel). + début de chaque appel). **Seconde manifestation mesurée (25/08/2026, révision + du lot 5)** : sans aucune bascule de profil, une rafale d'appels concurrents + pendant le chargement paresseux du cache workflow renvoie des résultats + faux en silence — `search_workflows("CST_", application: "CustomApp")` a + répondu `count: 0` (contre 44 en séquentiel), et `get_workflow_details` une + réponse anormale — les chargements concurrents du même cache ne sont pas + synchronisés (pas de déduplication de fetch en vol). Rejoués en séquentiel, + les mêmes appels sont corrects. Piste supplémentaire : mémoriser la promesse + de fetch en cours par clé de cache et la partager entre appelants. - **`select_expression`** : les projections via le paramètre `Select` provoquent des erreurs de compilation côté serveur (D13). Irritant principal restant. - **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH diff --git a/docs/handoff-lot5.md b/docs/handoff-lot5.md deleted file mode 100644 index 88117fe..0000000 --- a/docs/handoff-lot5.md +++ /dev/null @@ -1,242 +0,0 @@ -# Passation — lot 5 (clore la famille D24) - -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) — -en particulier **D24**, le contrat de troncature que ce lot généralise — avant -de toucher au code. - -**Mission** : éliminer les derniers cas connus de réponses d'outils dépassant -le seuil de rejet des clients MCP (~70 000 caractères, D24) — lot 5 de -[../ROADMAP.md](../ROADMAP.md) : pagination du blob `data` de -`get_workflow_details` (L5.1), garde de taille sur les trois outils de requête -(L5.2), champ `tool` manquant dans les enveloppes d'erreur locales (L5.3), -découvrabilité des applications dans les réponses de recherche (L5.4). - -**Hors périmètre** : tout le reste de la roadmap (L4.3, L4.4, L4.5, -exploration Metrics). **`QueryExecuteStream` est explicitement interdit** — -piste long terme consignée en L4.5, pas ce lot. Aucun nouvel outil : le compte -reste à 23. 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). 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);}});" -``` - -Mesurer la taille d'une réponse d'outil (la mesure qui fait foi est la -longueur de `content[0].text` via le protocole) : - -```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 | 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('chars:',m.result.content[0].text.length);}});" -``` - -## Contraintes non négociables - -1. `console.error()` uniquement (D6). -2. Contrat d'erreur : `{ success: false, error, tool }`, `isError: true` — - c'est précisément l'objet de L5.3. -3. Messages et hints 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 — tout paramètre ajouté (fenêtre de L5.1…) doit être déclaré - dans l'`inputSchema`. Le wrapper ne valide pas les **valeurs** : les gardes - de valeur vivent dans le code de l'outil. -6. **D24** : signal commun — `truncated: true` **uniquement** quand la réponse - est coupée, `hint` actionnable, `returned` vs total avant coupe. `omitted` - en plus quand des éléments entiers sont écartés. Réutilise ce vocabulaire - exactement ; ne crée pas un second dialecte. -7. **Pas de nouveau numéro de décision attendu** : ce lot **étend D24** - (complète son texte : les outils de requête et `get_workflow_details` - entrent dans son périmètre). Si une décision réellement nouvelle s'impose, - vérifie le dernier numéro (D26 à ce jour) et réserve D27 dans le commit qui - l'acte. - ---- - -## Phase 0 — Confirmer les mesures - -Toutes rejouées le 25/08/2026 (protocole, LIMAGRAIN) avec la commande de -mesure ci-dessus. À **confirmer**, pas à réinvestiguer : - -| Appel | Taille constatée | -|---|---:| -| `query_wms_entities` `{"entity_type":"Products","limit":200}` (Reading) | **957 234** | -| `search_wms_data` `{"keyword":"PAL"}` | **847 543** | -| `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` — une seule ligne Writing | **95 288** | -| `get_workflow_details` sur `CST_SendRejectContainersToPK` (`application: "CustomApp"`) | ~101 800 | -| `get_workflow_details` `{"workflow_id":"2a320000-0642-47df-aa52-3b89c27c016f"}` (StackerCrane, EasyWMS) | 79 092 (dont blob `data` : 71 512) | - -Attention aux noms de paramètres (D23 les fait respecter) : `search_workflows` -prend `query`, `search_wms_data` et `search_logs` prennent `keyword`. Pour -trouver l'id du workflow CST : -`search_workflows {"query":"CST_SendRejectContainersToPK","application":"CustomApp"}`. - ---- - -## L5.1 — Paginer le blob `data` de `get_workflow_details` - -**Problème.** La réponse embarque la définition complète du workflow (blob -`data`, XML/JSON EasyBuilder) : 71 512 caractères sur le StackerCrane mesuré, -davantage sur les gros `CST_*` — la réponse dépasse le seuil client. - -**À faire.** -- Deux paramètres de fenêtre sur le blob `data` (au schéma, D23) : une taille - max de tranche (défaut de l'ordre de **20 000** caractères, cohérent avec - D24) et un offset (défaut 0). Nommage à ta main (`max_data_chars` / - `data_offset` ou équivalent), documenté dans les descriptions. -- La réponse porte toujours la taille **totale** du blob ; quand la fenêtre - tronque : `truncated: true` + `hint` donnant l'offset suivant. -- La tranche est **verbatim** : découpe de chaîne, rien d'autre. -- Les métadonnées du workflow (`id`, `name`, `commonInfo`…) restent complètes - dans chaque réponse ; seule `data` est fenêtrée. - -**Pente naturelle interdite** : ne résume pas, ne reformule pas, ne « parse » -pas le blob pour n'en renvoyer que des morceaux jugés utiles — la définition -EasyBuilder doit rester reconstituable à l'octet près en concaténant les -tranches. - -**Vérification attendue** (protocole, LIMAGRAIN) : -- `get_workflow_details` sur le StackerCrane (`2a320000-0642-47df-aa52-3b89c27c016f`) - sans paramètre de fenêtre → réponse < ~30 000 caractères, `truncated: true`, - taille totale annoncée = 71 512, hint avec l'offset suivant. -- En enchaînant les tranches (offset 0, 20 000, 40 000, 60 000) : la somme des - longueurs des tranches = 71 512, et la concaténation est identique au blob - d'origine (compare au moins les longueurs et les 100 premiers/derniers - caractères). -- Un workflow à petit blob (< défaut) → réponse strictement inchangée, pas de - `truncated`. - -## L5.2 — Garde de taille sur les outils de requête - -**Problème.** Aucune borne de volume sur `query_wms_entities`, `call_query_api` -et `search_wms_data` : 957 234 caractères pour 200 lignes Reading, 847 543 -pour une recherche, et **95 288 pour une seule ligne Writing** (le modèle -Writing sérialise l'agrégat complet — navigations, `$id`… — là où la même -ligne Reading fait ~4 500). - -**À faire.** -- Garde de taille commune aux trois outils : plafond en variable - d'environnement avec défaut (style `MAX_LOG_SEARCH_CHARS`, 25 000 — même - ordre de grandeur, nom à ta main, documenté dans CLAUDE.md). -- Au-delà du plafond : écarter des **lignes entières** (pour `search_wms_data` : - des résultats entiers, par entité), et signaler D24 : `truncated: true`, - `returned`, `omitted`, `hint` (réduire `limit`, ajouter un `filter` ; et pour - `query_type != 0` : rappeler que les lignes Writing sont des agrégats - complets et suggérer Reading si l'usage le permet). -- **Cas limite à traiter explicitement** : une seule ligne dépasse le plafond - (réel en Writing). La réponse est alors `returned: 0`, `omitted: `, - `truncated: true`, avec un hint qui explique pourquoi et quoi faire — c'est - moins bon qu'un résultat, mais c'est mieux qu'un rejet client opaque. -- **Ne change ni `MAX_QUERY_ROWS`, ni les limites par défaut des outils, ni le - comportement sous le plafond** : une requête qui tient aujourd'hui doit - renvoyer exactement la même réponse. -- L'avertissement de volume mérite une phrase dans la description du paramètre - `query_type` (D25). - -**Vérification attendue** (protocole, LIMAGRAIN) : -- `query_wms_entities` `{"entity_type":"Products","limit":200}` → réponse sous - le plafond (+ marge d'enveloppe), `truncated: true`, `returned` < 200, - `omitted` cohérent, hint présent. -- `search_wms_data` `{"keyword":"PAL"}` → borné et signalé de même. -- `call_query_api` `{"entity_type":"Products","query_type":1,"limit":1}` → - `returned: 0`, `omitted: 1`, `truncated: true`, hint expliquant le volume - Writing. -- `query_wms_entities` `{"entity_type":"Container","limit":1}` → réponse - **strictement identique** à aujourd'hui (~4 500 caractères, pas de - `truncated`). -- `count_wms_entities` → non concerné, inchangé. - -## L5.3 — Champ `tool` dans les enveloppes d'erreur locales - -**Problème.** Les `catch` locaux d'`api-tools.js` (deux blocs, vers les lignes -145-162 et 191-208) renvoient `{ success: false, error }` **sans** le champ -`tool` du contrat. Preuve : `call_query_api` `{"entity_type":"Products","query_type":7}` -répond aujourd'hui `{"success": false, "error": "query_type invalide : …"}` — -pas de `tool`. - -**À faire.** Balayer **tous** les modules de `src/tools/` à la recherche -d'enveloppes `success: false` construites localement : soit y ajouter `tool`, -soit laisser l'erreur remonter au wrapper de `src/index.js` (qui l'ajoute) — -au choix selon le cas, mais le résultat observable est uniforme. Attention à ne -pas perdre les champs additionnels utiles des enveloppes locales (le `warning` -de résolution d'`api-tools`, les listes de profils de `profile-tools`…). - -**Vérification attendue** : `call_query_api` avec `query_type: 7` → l'enveloppe -contient `"tool": "call_query_api"` ; un `grep` sur `src/tools/` ne montre plus -d'enveloppe `success: false` sans `tool` (colle le résultat du grep dans le -compte-rendu). - -## L5.4 — Découvrabilité des applications dans les réponses de recherche - -**Problème (cas réel du 25/08/2026).** Une session Cowork cherchant des -workflows `CST_*` sans passer `application: "CustomApp"` a conclu à tort que -l'AD n'en contenait aucun — alors que `CST_PickingTasksSequencing_PR` et -`CST_ChooseDestinationFromPS` existent (vérifié : -`search_workflows {"query":"CST_PickingTasksSequencing","application":"CustomApp"}` -→ 1 résultat). Le paramètre `application` (D26) existe, mais rien dans la -**réponse** ne dit qu'on n'a interrogé qu'une application sur neuf. - -**À faire.** Dans les réponses de `search_workflows` et `search_ad_elements` : -- toujours rappeler l'application interrogée (champ `application`) ; -- quand la recherche renvoie **peu ou pas** de résultats (seuil à ta main, 0 - au minimum), ajouter un hint nommant les autres applications déclarées et le - paramètre `application`. La liste vient de la **liste allégée - d'`Application/GetAll` déjà en cache** (D26) — n'ajoute aucun appel réseau ni - préchargement pour construire ce hint ; si la liste n'est pas encore en - cache, le hint générique (« d'autres applications existent — - `list_workflow_categories` pour les voir ») suffit. - -**Vérification attendue** (protocole, LIMAGRAIN) : -- `search_workflows {"query":"CST_"}` (sans `application`) → 0 résultat **avec** - un hint nommant `CustomApp` (ou renvoyant vers `list_workflow_categories`). -- `search_workflows {"query":"CST_","application":"CustomApp"}` → résultats - peuplés, champ `application: "CustomApp"` dans la réponse. -- `search_workflows {"query":"stacker"}` → résultats inchangés par ailleurs, - champ `application: "EasyWMS"` présent, pas de hint parasite. -- stderr : aucun fetch d'une application non demandée. - ---- - -## Méthode - -1. Phase 0 d'abord (cinq mesures, rejouées telles quelles). -2. L5.1, puis L5.2, puis L5.3, puis L5.4 — chaque bloc vérifié **en exécution - via le protocole** avant de passer au suivant. -3. Rappel : le serveur traite les `tools/call` en concurrence (point ouvert de - la ROADMAP) — pour les vérifications qui comparent des réponses successives, - envoie les requêtes séquentiellement. -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 (L5.1, L5.2, L5.3, L5.4), messages expliquant le pourquoi, - **mesures avant/après dans le corps du message** (tailles en caractères). -- Documentation dans les mêmes commits : **compléter D24** (périmètre étendu - aux outils de requête et à `get_workflow_details`) ; CLAUDE.md — la nouvelle - variable d'environnement dans les réglages partagés, la fenêtre de - `get_workflow_details` si elle change l'usage documenté ; ROADMAP.md — - retirer le lot 5. -- 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 tailles et les sorties), plus la baseline - finale. Laisse `handoff-lot5.md` en place pour la révision.