diff --git a/ROADMAP.md b/ROADMAP.md index 2726686..c30bb47 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -84,6 +84,41 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et --- +## Lot 5 — Clore la famille D24 (rejets client sur sorties volumineuses) + +Mesures du 25/08/2026 (protocole, LIMAGRAIN), toutes au-dessus du seuil de +rejet client (~70 000 caractères, D24) : + +| Appel | Taille | +|---|---:| +| `query_wms_entities("Products", limit: 200)` — Reading ordinaire | **957 234** | +| `search_wms_data("PAL")` | **847 543** | +| `get_workflow_details(CST_SendRejectContainersToPK)` | ~101 800 | +| `call_query_api("Products", query_type: 1, limit: 1)` — **une seule ligne** Writing | **95 288** | + +### L5.1 — Paginer le blob `data` de `get_workflow_details` + +La définition complète d'un workflow dépasse le seuil (~101 800 pour +`CST_SendRejectContainersToPK`, 79 092 pour un `StackerCrane_…` EasyWMS). +Découper le blob `data` en tranches verbatim (paramètres de fenêtre au schéma), +signalées D24 — sans jamais résumer ni reformuler le contenu. + +### L5.2 — Garde de taille sur les outils de requête + +Une ligne Writing = un agrégat complet sérialisé (95 288 caractères là où la +même ligne Reading en fait ~4 500) ; 200 lignes Reading = ~957 000 ; `search_wms_data` += ~848 000. Garde de taille commune sur `query_wms_entities`, `call_query_api` +et `search_wms_data` : lignes entières écartées, `truncated`/`returned`/`omitted`/`hint` +(D24). Ne pas réduire `MAX_QUERY_ROWS` ni les limites par défaut. + +### L5.3 — Champ `tool` absent des erreurs construites localement + +Les `catch` locaux d'`api-tools.js` renvoient `{ success: false, error }` sans +le champ `tool` du contrat (convention 3) — antérieur au lot 4. Balayer tous +les modules d'outils pour le même motif. + +--- + ## Écarté | Proposition | Raison | @@ -118,26 +153,6 @@ plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et début de chaque appel). - **`select_expression`** : les projections via le paramètre `Select` provoquent des erreurs de compilation côté serveur (D13). Irritant principal restant. -- **`get_workflow_details` peut dépasser le seuil de rejet client** (constaté - le 25/08/2026, livraison du lot 4). La définition complète de - `CST_SendRejectContainersToPK` (application `CustomApp`) fait ~101 800 - caractères via le protocole — au-delà du seuil de rejet mesuré en D24 - (~70 000). Comportement antérieur au lot 4 (les grosses définitions - `EasyWMS` sont dans le même cas) : à borner et signaler (`truncated`/`hint`, - D24) dans un lot futur. -- **Une ligne Writing est énorme** (mesuré le 25/08/2026, révision du lot 4). - `call_query_api("Products", query_type: 1, limit: 1)` → **95 288 caractères - pour une seule ligne** : le modèle Writing sérialise l'agrégat complet - (navigations, `$id`…), là où la même ligne en Reading pèse ~4 500 caractères. - Au-delà du seuil de rejet client (~70 000, D24) dès `limit: 1`. À traiter - avec le point `get_workflow_details` ci-dessus : même famille D24 (borner et - signaler), et l'avertissement mérite d'apparaître dans la description du - paramètre `query_type`. -- **Champ `tool` absent des erreurs construites localement** (constaté le - 25/08/2026). Les `catch` locaux d'`api-tools.js` renvoient - `{ success: false, error }` sans le champ `tool` du contrat (convention 3) — - motif antérieur au lot 4. Cosmétique : soit laisser l'erreur remonter au - wrapper (qui ajoute `tool`), soit l'ajouter aux enveloppes locales. - **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH reste à faire. - **Historique des shipment templates** : hors de portée, les logs concernés diff --git a/docs/handoff-lot5.md b/docs/handoff-lot5.md new file mode 100644 index 0000000..f8f1141 --- /dev/null +++ b/docs/handoff-lot5.md @@ -0,0 +1,212 @@ +# 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). + +**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). + +--- + +## Méthode + +1. Phase 0 d'abord (cinq mesures, rejouées telles quelles). +2. L5.1, puis L5.2, puis L5.3 — 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), 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.