Compare commits

..

4 Commits

Author SHA1 Message Date
Arthur Ria 702c2ecd2a Supprime handoff-lot3.md : passation livrée et révisée
Révision du lot 3 : les sept vérifications attendues rejouées via le
protocole passent (21 404 chars / 50 sur 168 paginés avec hint, plafond
25 000 respecté sur search_logs avec truncated + omitted, D23 intact,
limit 200 = opt-in explicite au volume). Baseline 23/6, npm test 4/4
exit 0, diffs propres, D24 conforme aux mesures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:49:59 +02:00
Arthur Ria b37c2ac251 L3.1c : acte le contrat de troncature en D24, retire le lot 3 de la ROADMAP
Les deux bornages (L3.1a pagination, L3.1b plafond de volume) partagent
le même vocabulaire de signal — truncated présent uniquement quand la
réponse est coupée, hint actionnable, returned vs total avant coupe —
mais gardent des implémentations locales : paginer et plafonner un
volume sont deux mécanismes distincts, un helper commun forcerait une
abstraction qu'ils n'ont pas. D24 consigne ce contrat, les garde-fous
de cadrage (défauts inchangés, mesure protocolaire qui fait foi) et le
changement de sens de totalParameters.

ROADMAP : L3.1 livré, le lot 3 devenait vide — section retirée ; la
référence à L3.1 dans L4.5 (QueryExecuteStream) renvoie désormais à
D24. read_recent_logs est laissé tel quel : sans helper partagé, rien
de gratuit à lui apporter (L3.1c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:36:37 +02:00
Arthur Ria d0a6cc1a0b L3.1b : plafonne la taille de réponse de search_logs (MAX_LOG_SEARCH_CHARS)
max_results (50) borne le nombre de résultats mais pas le volume : les
context_lines multiplient la taille, et la réponse aux seuls défauts
atteignait 52-56 000 caractères — rejetée par le client MCP. Le tool
écarte désormais des résultats ENTIERS (jamais coupés au milieu de leur
contexte) jusqu'à passer sous le plafond, et le signale : truncated,
returned/omitted, hint actionnable (affiner keyword, réduire
context_lines, baisser max_results).

Plafond : MAX_LOG_SEARCH_CHARS, variable d'environnement avec défaut
25 000 — même style de lecture que MAX_QUERY_ROWS/QUERY_TIMEOUT
(parseInt(process.env.X) || défaut), documentée dans CLAUDE.md
(réglages partagés). 25 000 correspond à l'ordre de grandeur cible du
lot (~20-25 000 chars) et se mesure sur content[0].text, la taille qui
fait foi côté protocole. Les défauts max_results=50 et context_lines=2
sont inchangés : le correctif est le bornage signalé, pas un changement
de comportement par défaut.

Mesures via le protocole (LIMAGRAIN, content[0].text) :
- {"keyword":"Error"} : avant 52 162 chars / 50 résultats ; après
  24 164 chars, returned 16, omitted 34, truncated true, hint présent.
- {"keyword":"Error","max_results":3} : 4 855 chars, 3/3, pas de
  troncature signalée.
- mot-clé sans occurrence : 112 chars, 0 résultat, pas de truncated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:35:49 +02:00
Arthur Ria c4d6b5650e L3.1a : pagine get_system_parameters (limit/offset, signal truncated)
La sortie sans filtre (168 paramètres fusionnés) atteignait 70 141
caractères et se faisait rejeter par le client MCP. Ajout de limit
(défaut 50) et offset (défaut 0), déclarés au schéma (D23), appliqués
après filtres et tri. Quand la pagination coupe, la réponse porte
truncated: true et un hint donnant l'offset suivant et les filtres
pour réduire.

totalParameters change de sens : c'était le nombre brut d'entités
Parameter chargées (toujours 168), c'est désormais le total
correspondant aux filtres AVANT pagination — le signal truncated se
vérifie ainsi depuis la réponse (offset + returned < totalParameters).
Sans filtre les deux définitions coïncident.

Mesures via le protocole (LIMAGRAIN, content[0].text) :
- {} : avant 70 141 chars / 168 renvoyés ; après 21 404 chars,
  returned 50, totalParameters 168, truncated true, hint présent.
- {"limit":200} : 70 156 chars, les 168, pas de truncated (opt-in
  explicite au volume complet).
- {"offset":160} : 3 503 chars, 8 renvoyés, pas de truncated.
- {"search":"PICK"} : 10 041 chars, 23/23, pas de truncated —
  comportement inchangé sur petit résultat.
- {"lines":5} : toujours rejeté par le wrapper (D23 intact).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:34:23 +02:00
6 changed files with 120 additions and 216 deletions
+4
View File
@@ -146,6 +146,10 @@ explicite (D9). Par défaut `false`.
**`LOGS_PATH`** accepte le placeholder `{host}`, substitué par le host du profil
actif à chaque appel.
**`MAX_LOG_SEARCH_CHARS`** (défaut 25 000) : plafond en caractères de la réponse
de `search_logs` — au-delà, des résultats entiers sont écartés et signalés
(`truncated`, D24).
**Au runtime.** `profile-manager` est un singleton d'état global. Les services
s'abonnent via `onSwitch()` pour invalider ce qui dépend du tenant :
+40
View File
@@ -440,3 +440,43 @@ silence, pas de réimplémenter JSON Schema.
Le renommage des paramètres (`entity_type`/`query` uniformisés) a été **écarté**
au profit de cette validation — voir ROADMAP « Écarté ».
---
## D24 — Contrat de troncature : borné + signalé, jamais un rejet silencieux
**Piège mesuré (24-25/08/2026).** Une réponse d'outil de ~70 000 caractères
(`get_system_parameters` sans filtre ; `search_logs` atteignait 52-56 000 avec
les seuls défauts) est **rejetée par le client MCP** — l'utilisateur voit un
échec opaque au lieu d'un résultat partiel.
**Décision.** Tout outil susceptible de produire une sortie volumineuse borne
sa réponse et **signale** la coupe. Le signal est commun :
| Champ | Sémantique |
|---|---|
| `truncated: true` | présent **uniquement** quand la réponse a été coupée — jamais `truncated: false` |
| `hint` | présent ssi `truncated` ; actionnable : dit comment continuer (`offset` suivant) ou réduire (filtres, `context_lines`…) |
| `returned` | nombre d'éléments effectivement renvoyés |
| total (`totalParameters`, `totalResults`) | total **avant** la coupe — `truncated` se vérifie donc depuis la réponse elle-même |
Les mécanismes restent **volontairement locaux**, car ils diffèrent :
`get_system_parameters` pagine (`limit`/`offset` au schéma — rien n'est perdu,
on continue avec l'offset suivant) ; `search_logs` plafonne le volume
(`MAX_LOG_SEARCH_CHARS`, défaut 25 000 caractères) en écartant des résultats
**entiers** — jamais coupés au milieu de leurs lignes de contexte — et annonce
en plus `omitted`, le compte écarté. Pas de helper partagé : le factoriser
forcerait une abstraction commune à deux mécanismes qui n'en ont pas.
Deux garde-fous de cadrage :
- **Ne pas réduire les défauts existants** (`max_results` 50, `context_lines` 2)
pour passer sous le plafond : le correctif est le bornage signalé, pas un
changement silencieux de comportement.
- La taille qui fait foi est celle de `content[0].text` **mesurée via le
protocole**, pas une estimation. Ordre de grandeur cible : ~20-25 000
caractères par réponse.
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
filtres avant pagination (identique sans filtre).
+1 -17
View File
@@ -12,22 +12,6 @@ contient que ce qui reste à faire.
---
## Lot 3 — Ergonomie et documentation
Le lot 2 (résolution `Name` -> `TableName`, rejet des paramètres inconnus,
garde-fous d'arguments manquants) est livré — voir **D21** et **D23**.
### L3.1 — Bornage des sorties volumineuses
- `get_system_parameters` : ajouter `limit` / `offset`, aujourd'hui absents
(sortie constatée : 70 000 caractères, rejetée par le client).
- `search_logs` : garde-fou de taille. `max_results` existe déjà, mais les
`context_lines` multiplient le volume (88 000 caractères pour 50 résultats).
- Renvoyer `truncated: true` explicitement plutôt que de laisser le client se
faire rejeter.
---
## Lot 4 — Modèle de données et applications
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
@@ -151,7 +135,7 @@ La référence de l'API documente des champs que le MCP n'envoie jamais :
| `Parameters` | requêtes **paramétrées** (dictionnaire `nom -> {TypeName, Value}`) — supprimerait toute concaténation de chaîne dans les filtres, et pourrait débloquer D13 (`Select`) |
| `CommandTimeout` | timeout par requête, au lieu du timeout HTTP global de 30 s |
| `QueryId` + `POST /QueryCancel` | annulation d'une requête longue |
| `POST /QueryExecuteStream` | résultats en flux — piste sérieuse pour L3.1 (sorties volumineuses) |
| `POST /QueryExecuteStream` | résultats en flux — piste long terme pour les sorties volumineuses, au-delà du bornage signalé de D24 |
Autres endpoints jamais utilisés, à évaluer : `QueryEvents`, `QueryCommands`,
`QueryCorrelationEvents`, `QuerySnapshots` (event sourcing — utile en debug),
-175
View File
@@ -1,175 +0,0 @@
# Passation — lot 3 (L3.1, bornage des sorties volumineuses)
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** : borner les sorties des outils qui produisent aujourd'hui des
réponses de 50 000 à 70 000 caractères, rejetées par les clients MCP —
`get_system_parameters` et `search_logs` — avec un signal `truncated: true`
explicite plutôt qu'un rejet silencieux côté client.
**Hors périmètre** : tout le reste de la roadmap (lot 4 en entier). En
particulier, **n'explore pas `QueryExecuteStream`** — c'est une piste long
terme consignée en L4.5, pas ce lot. Ne touche pas aux limites des outils de
requête (`MAX_QUERY_ROWS` fait déjà le travail). 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`, `saas=false` — les outils de logs y fonctionnent.
- **Le profil `AD` est cassé et c'est diagnostiqué — ne le réinvestigue pas**
(tenant introuvable côté STS, point ouvert de la ROADMAP). Conséquence :
`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);}});"
```
Mesurer la taille d'une réponse d'outil via le protocole (c'est la mesure qui
fait foi, pas une estimation) :
```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 — 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 et hints actionnables : dire quoi faire ensuite.
4. Aucun accès base de données (D1).
5. **D23 : le wrapper `tools/call` valide les arguments contre les schémas.**
Tout paramètre que tu ajoutes (`limit`, `offset`…) doit être déclaré dans
l'`inputSchema` de l'outil, sinon le wrapper le rejettera comme inconnu.
C'est voulu — ne contourne pas la validation.
6. Les schémas portent `additionalProperties: false` — conserve-le.
**Numéro de décision réservé** : **D24** = contrat de troncature (si tu actes
un contrat commun — voir L3.1c). Vérifie que D23 est bien la dernière décision
avant d'écrire.
---
## Phase 0 — Confirmer les deux mesures
Mesures du 25/08/2026 (profil `LIMAGRAIN`, via le protocole) à **confirmer**
avec la commande de mesure ci-dessus, pas à réinvestiguer :
| Appel | Constaté |
|---|---|
| `get_system_parameters` `{}` | **70 141 caractères**, 168 paramètres, aucun `limit`/`offset` au schéma |
| `search_logs` `{"keyword":"Error"}` (défauts : `max_results` 50, `context_lines` 2) | **55 954 caractères**, 50 résultats |
Contexte : une sortie de ~70 000 caractères a déjà été **rejetée par le client
MCP** (constat du 24/08/2026 qui a motivé ce lot). L'ordre de grandeur cible
est ~20 00025 000 caractères par réponse ; c'est un ordre de grandeur, pas un
chiffre sacré — ce qui compte est le comportement (borné + signalé), mesuré via
le protocole.
---
## L3.1a — `get_system_parameters` : pagination
**Problème.** L'outil (`src/tools/config-tools.js`) renvoie les 168 paramètres
fusionnés d'un bloc : 70 141 caractères sans filtre. Les filtres existants
(`warehouse`, `param_class`, `search`, `only_overridden`) réduisent la sortie
mais rien ne borne le cas sans filtre.
**À faire.** Ajouter `limit` (défaut raisonnable, ~50 — à ce volume la réponse
tient vers 21 000 caractères) et `offset` (défaut 0), **déclarés au schéma**
(contrainte 5). La réponse annonce toujours `totalParameters` (le total avant
pagination), le nombre renvoyé, et — quand la pagination a tronqué —
`truncated: true` avec un hint indiquant comment continuer (`offset` suivant)
ou réduire (`param_class`, `search`).
**Vérification attendue** (protocole, LIMAGRAIN) :
- `{}` → taille < 25 000 caractères, 50 paramètres renvoyés,
`totalParameters: 168`, `truncated: true`, hint présent.
- `{"limit": 200}` → les 168, `truncated: false` (ou champ absent — mais alors
cohérent partout).
- `{"offset": 160}` → 8 paramètres, pas de `truncated`.
- `{"search": "CROSSDOCK"}` → comportement inchangé sur petit résultat.
- `{"lines": 5}` → toujours rejeté par le wrapper (D23 intact).
## L3.1b — `search_logs` : garde-fou de taille
**Problème.** `max_results` existe (défaut 50) mais ne borne pas le **volume** :
les `context_lines` multiplient la taille des résultats. Mesuré : 55 954
caractères avec les seuls défauts.
**À faire.** Un garde-fou sur la taille cumulée de la réponse construite
(`src/tools/log-tools.js` / `src/services/log-service.js`) : au-delà du
plafond, couper la liste des résultats — des résultats **entiers**, ne coupe
pas un résultat au milieu de ses lignes de contexte — et poser
`truncated: true` + un compte des résultats retenus/écartés + un hint
(réduire `context_lines`, affiner `keyword`, baisser `max_results`).
Plafond : constante ou variable d'environnement avec défaut (cohérent avec le
style `MAX_QUERY_ROWS`/`QUERY_TIMEOUT` dans le code existant) — documente le
choix dans le commit.
**Pente naturelle interdite** : ne réduis pas silencieusement les défauts
(`max_results` 50, `context_lines` 2 restent tels quels) — le correctif est le
bornage signalé, pas un changement de comportement par défaut qui casserait
les usages existants.
**Vérification attendue** (protocole, LIMAGRAIN) :
- `{"keyword":"Error"}` → taille sous le plafond choisi, `truncated: true`,
compte écarté + hint présents.
- `{"keyword":"Error","max_results":3}` → petit, pas de troncature signalée.
- Un mot-clé sans occurrence → comportement inchangé (0 résultat, pas de
`truncated`).
## L3.1c — Cohérence du signal `truncated`
Si tu factorises un helper de troncature commun aux deux outils, actes le
contrat en **D24** dans DECISIONS.md (forme du signal : `truncated: true`,
compte total vs renvoyé, hint actionnable). Si les deux implémentations restent
locales et divergentes, harmonise au moins les noms de champs — deux
vocabulaires pour le même concept est exactement le genre de dérive qu'on
traque en révision. `read_recent_logs` peut bénéficier du même helper si c'est
gratuit ; ne le complexifie pas pour ça.
---
## Méthode
1. Phase 0 d'abord (deux mesures, rejouées telles quelles).
2. L3.1a puis L3.1b puis L3.1c. Chaque correctif vérifié **en exécution via le
protocole** avant de passer au suivant — la taille en caractères de
`content[0].text` est la mesure qui fait foi.
3. Les schémas changent (nouveaux paramètres) : 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), et vérifier qu'un
paramètre inconnu est toujours rejeté (D23).
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 (L3.1a, L3.1b, L3.1c si D24), messages expliquant le
pourquoi, **mesures avant/après dans le corps du message** (tailles en
caractères, rejouées via le protocole).
- Documentation dans les mêmes commits : D24 si actée ; ROADMAP.md — retirer
L3.1 (le lot 3 devient vide : retire la section) ; CLAUDE.md ne change que si
tu ajoutes une variable d'environnement (la documenter dans la section des
réglages partagés).
- 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 les tailles et les sorties), plus la
baseline finale.
+43 -9
View File
@@ -49,6 +49,16 @@ Examples:
description: 'If true, return only parameters that have at least one warehouse override (default: false).',
default: false,
},
limit: {
type: 'number',
description: 'Maximum number of parameters returned per call (default: 50). The full unfiltered list is ~70,000 characters — raise this only if you really need everything at once.',
default: 50,
},
offset: {
type: 'number',
description: 'Number of matching parameters to skip, for pagination (default: 0). Combine with limit to walk the full list.',
default: 0,
},
},
},
},
@@ -64,9 +74,18 @@ async function executeTool(name, args) {
}
}
const DEFAULT_PARAMS_LIMIT = 50;
async function getSystemParameters(args) {
const { warehouse, param_class, search, only_overridden = false } = args || {};
// Bornes de pagination — valeurs invalides ramenées aux défauts, la
// validation du wrapper (D23) ne contrôle que les noms de paramètres.
const rawLimit = Number(args && args.limit);
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.floor(rawLimit) : DEFAULT_PARAMS_LIMIT;
const rawOffset = Number(args && args.offset);
const offset = Number.isFinite(rawOffset) && rawOffset >= 0 ? Math.floor(rawOffset) : 0;
try {
// Both entities are small (a few hundred rows max) — fetch fully and merge
// client-side to avoid LINQ string-injection and null-field pitfalls.
@@ -124,18 +143,33 @@ async function getSystemParameters(args) {
rows.sort((a, b) => String(a.code).localeCompare(String(b.code)));
// Pagination (L3.1) : sans elle la sortie sans filtre atteint ~70 000
// caractères et se fait rejeter par les clients MCP. totalParameters est
// le total correspondant aux filtres, AVANT pagination — le signal
// truncated se vérifie donc depuis la réponse : offset + returned < total.
const matched = rows.length;
const page = rows.slice(offset, offset + limit);
const truncated = offset + page.length < matched;
const payload = {
success: true,
warehouse: warehouse || '(none — effective value = default)',
filters: { param_class: param_class || null, search: search || null, only_overridden },
totalParameters: matched,
totalOverrides: Array.isArray(paramValues) ? paramValues.length : 0,
returned: page.length,
offset,
};
if (truncated) {
payload.truncated = true;
payload.hint = `Showing parameters ${offset + 1}-${offset + page.length} of ${matched}. Call again with offset=${offset + page.length} for the next page, or narrow the result with param_class / search.`;
}
payload.parameters = page;
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
warehouse: warehouse || '(none — effective value = default)',
filters: { param_class: param_class || null, search: search || null, only_overridden },
totalParameters: Array.isArray(parameters) ? parameters.length : 0,
totalOverrides: Array.isArray(paramValues) ? paramValues.length : 0,
returned: rows.length,
parameters: rows,
}, null, 2),
text: JSON.stringify(payload, null, 2),
}],
};
} catch (err) {
+32 -15
View File
@@ -182,22 +182,39 @@ async function searchLogs(args) {
const result = await logService.searchLogs(keyword, max_results, context_lines);
// Garde-fou de taille (L3.1) : max_results borne le nombre de résultats,
// pas le volume — les context_lines multiplient la taille (55 954 chars
// mesurés avec les seuls défauts, rejetés par le client MCP). Au-delà du
// plafond on écarte des résultats ENTIERS (jamais coupés au milieu de
// leur contexte) et on le signale : truncated + omitted + hint.
const cap = parseInt(process.env.MAX_LOG_SEARCH_CHARS) || 25000;
const buildText = (kept) => {
const omitted = result.results.length - kept.length;
const payload = {
success: true,
keyword: result.keyword,
totalResults: result.totalResults,
returned: kept.length,
};
if (omitted > 0) {
payload.truncated = true;
payload.omitted = omitted;
payload.hint = `Plafond de taille de réponse atteint (${cap} caractères) : ${kept.length} résultat(s) renvoyé(s) sur ${result.totalResults}, ${omitted} écarté(s). Affinez le keyword, réduisez context_lines ou baissez max_results.`;
}
payload.results = kept;
return JSON.stringify(payload, null, 2);
};
let kept = result.results.slice();
let text = buildText(kept);
while (text.length > cap && kept.length > 0) {
kept.pop();
text = buildText(kept);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: true,
keyword: result.keyword,
totalResults: result.totalResults,
results: result.results,
},
null,
2
),
},
],
content: [{ type: 'text', text }],
};
} catch (err) {
return {