cb625a7918
Context.{entity_type} attend le TableName du Metadata, pas le nom
d'entité de l'AD (Container -> Containers, mais Alias -> Alias) : un nom
faux partait en HTTP 500 de compilation LINQ. Nouveau service
entity-resolver.js : table Name|TableName (insensible à la casse) ->
TableName, agrégée sur les applications déployées (via
GET /configuration/applications — les applications sans contexte
requêtable n'y figurent pas et n'apportent 0 entité Metadata), cache TTL
partagé, invalidation par onSwitch (D8). Branché dans wms-query-service
(query/count/schema/search) et call_query_api.
Nom inconnu -> échec avant tout appel réseau de requête, suggestions
proches + renvoi vers get_entity_metadata. Metadata injoignable -> le
nom passe tel quel avec un warning dans la réponse.
Mesures (LIMAGRAIN, via le protocole) :
- query_wms_entities("Container", limit 1) -> succès, 1 ligne, résolu
Containers
- query_wms_entities("Alias") -> succès, invariant (pas de pluriel)
- query_wms_entities("Item") -> "Item" n'existe pas dans le modèle
Reading. Proches : RFMenuItems, Sites. 288 entités disponibles —
aucune ligne [API] POST dans stderr
- count_wms_entities("Product") -> 51160
- get_entity_schema("Container") et call_query_api("Container") : mêmes
résolutions
- 288 TableName distincts sur 5 applications, aucun conflit
Name -> TableName (mesuré le 24/08/2026)
Docs : D21 dans DECISIONS.md ; CLAUDE.md (piège retiré des points
ouverts, liste d'entités corrigée Aliases -> Alias, entity-resolver dans
la structure) ; exemple singulier/pluriel dans wms://query-examples ;
ROADMAP allégée (cause racine + L2.1 + L3.3 livrés).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
417 lines
17 KiB
Markdown
417 lines
17 KiB
Markdown
# Décisions d'architecture et pièges vérifiés
|
|
|
|
Ce fichier consigne **pourquoi** le code est écrit comme il l'est. Chaque entrée
|
|
décrit une décision prise ou un piège constaté **sur un WMS réel** — pas une
|
|
supposition. Avant de « corriger » un comportement qui paraît étrange, cherchez-le
|
|
ici : il est probablement volontaire.
|
|
|
|
Convention : une décision reste dans le fichier même si elle est révisée ; on
|
|
ajoute alors une entrée `Révisée le …` plutôt que de réécrire l'histoire.
|
|
|
|
---
|
|
|
|
## D1 — 100 % API, aucun accès Oracle direct
|
|
|
|
**Décision.** Toutes les données transitent par les API REST du WMS. Aucune
|
|
connexion base de données.
|
|
|
|
**Pourquoi.** Le serveur MCP doit fonctionner depuis un poste ou une VM sans
|
|
credentials Oracle, sans client Oracle installé, et sans risque d'écriture
|
|
directe en base. L'API impose en prime les règles métier et les droits du
|
|
compte utilisé.
|
|
|
|
**Conséquence.** Les fichiers `src/services/oracle-service.js`,
|
|
`src/resources/database.js` et `src/tools/database-tools.js` ont été supprimés
|
|
(ils étaient de toute façon morts : non branchés, `oracledb` n'était même pas
|
|
une dépendance). **Ne pas les réintroduire.** Si une donnée n'est pas
|
|
atteignable par API, elle est hors périmètre — voir D16.
|
|
|
|
---
|
|
|
|
## D2 — OAuth : `tenant_code` est obligatoire
|
|
|
|
**Piège.** L'endpoint `/EasySTS/OAuth/Token` répond `400 Bad Request` si le
|
|
paramètre `tenant_code` est absent, sans message explicite.
|
|
|
|
**Solution.** Le corps du grant `password` contient toujours les quatre
|
|
paramètres :
|
|
|
|
```
|
|
grant_type=password&tenant_code=<TENANT>&username=<USER>&password=<PASS>
|
|
```
|
|
|
|
Voir `src/services/api-service.js`, méthode `authenticate()`.
|
|
|
|
---
|
|
|
|
## D3 — `QueryType: 0` (Reading), pas 1
|
|
|
|
**Piège.** `QueryType` (type `QueryContextType`) sélectionne le contexte de
|
|
données interrogé. Il a **quatre** valeurs, pas deux — vérifiées une à une sur
|
|
le tenant `LIMAGRAI2512` :
|
|
|
|
| Valeur | Contexte | Statut sur ce tenant |
|
|
|---|---|---|
|
|
| `0` | **Reading** — `ApplicationReadingContext` | opérationnel, champs de statut en **chaînes** (`"Release"`) |
|
|
| `1` | Writing — `ApplicationWritingRepository` | opérationnel, champs de statut en **énumérations** |
|
|
| `2` | DataWarehouse | **non configuré** : `Could not resolve serviceType 'IDataWarehouse…'` |
|
|
| `3` | Metrics — `ApplicationMetricDataContext` | contexte présent, modèle de données non exploré |
|
|
|
|
Les comparaisons de statut par chaîne — de loin le cas le plus courant en
|
|
debug — **échouent** en `QueryType: 1`. Le code force donc `0` dans
|
|
`executeQuery()` et `executeScalarQuery()`.
|
|
|
|
Le message d'erreur nomme le contexte (`ApplicationReadingContext`,
|
|
`ApplicationWritingRepository`, …) : c'est le moyen le plus rapide de savoir
|
|
quel `QueryType` a réellement été utilisé.
|
|
|
|
**Attention.** D'anciens exemples (dont le PHP de référence) utilisent `1`. Ne
|
|
les recopiez pas.
|
|
|
|
---
|
|
|
|
## D4 — Les API AD renvoient `{ entities: [...] }`, pas un tableau
|
|
|
|
**Piège.** `POST /AD/api/{Type}/GetByApplication` renvoie un objet enveloppe. Un
|
|
code qui traite la réponse comme un tableau obtient silencieusement
|
|
`0 élément` — le symptôme historique était « Successfully cached 0 workflows ».
|
|
|
|
**Solution.** Toujours extraire : `response?.entities || []`.
|
|
|
|
---
|
|
|
|
## D5 — Les propriétés arrivent en minuscules *ou* en majuscules
|
|
|
|
**Piège.** Selon le type d'élément et la version du WMS, l'API renvoie `name`
|
|
ou `Name`, `id` ou `Id`.
|
|
|
|
**Solution.** Systématiquement `const name = w.name || w.Name || ''` avant tout
|
|
filtrage ou tri. Une recherche qui « ne trouve pas » un élément qui existe est
|
|
presque toujours ce bug.
|
|
|
|
---
|
|
|
|
## D6 — dotenv doit écrire sur stderr
|
|
|
|
**Piège.** dotenv affiche une bannière de version sur **stdout**. Or le
|
|
protocole MCP réserve stdout au JSON : Claude Desktop échoue alors avec
|
|
`Unexpected token 'd', "[dotenv@17."... is not valid JSON`.
|
|
|
|
**Solution.** `src/index.js` détourne `process.stdout.write` vers stderr le
|
|
temps du chargement de dotenv, puis le restaure.
|
|
|
|
**Règle générale.** Dans tout le projet, on log avec `console.error()`.
|
|
**Jamais** `console.log()`.
|
|
|
|
---
|
|
|
|
## D7 — Le `.env` est lu à côté de l'exécutable quand le serveur est packagé
|
|
|
|
**Décision.** `src/index.js` résout le chemin du `.env` selon le contexte :
|
|
|
|
| Contexte | Chemin du `.env` |
|
|
|---|---|
|
|
| Sources (`npm start`) | racine du projet |
|
|
| Exécutable pkg (`process.pkg`) | dossier de `process.execPath` |
|
|
|
|
**Pourquoi.** Avec un chemin statique, pkg **embarque le `.env` dans le
|
|
snapshot** de l'exe : les credentials sont figés dans le binaire et
|
|
reconfigurer un déploiement impose un rebuild. Le chemin dynamique via
|
|
`process.execPath` empêche pkg de le détecter, donc rien n'est embarqué, et
|
|
`dist/.env` devient le fichier de configuration du déploiement.
|
|
|
|
**Vérification.** Sans `.env` à côté de l'exe, le serveur démarre en
|
|
avertissant `WMS_PROFILES is empty` — preuve qu'aucune valeur n'est embarquée.
|
|
|
|
---
|
|
|
|
## D8 — Multi-profils au runtime plutôt qu'un serveur MCP par WMS
|
|
|
|
**Décision.** Un seul serveur MCP dessert plusieurs backends WMS ; Claude bascule
|
|
avec `switch_wms_profile`.
|
|
|
|
**Pourquoi.** L'alternative — une entrée par client dans
|
|
`claude_desktop_config.json` — multiplie les processus, les jeux de credentials
|
|
et les caches, pour un usage où l'on ne consulte qu'un WMS à la fois.
|
|
|
|
**Conséquence.** L'état actif est **global au processus**. Un changement de
|
|
profil doit invalider tout ce qui dépend du tenant. Les services s'abonnent via
|
|
`profileManager.onSwitch()` :
|
|
|
|
| Service | Réaction au switch |
|
|
|---|---|
|
|
| `api-service` | `resetToken()` — le token OAuth appartient au tenant précédent |
|
|
| `workflow-service` | `clearCache()` |
|
|
| `ad-service` | `invalidateCache()` — tous les types |
|
|
|
|
**Ne jamais** appeler ces invalidations à la main depuis un autre module :
|
|
l'abonnement suffit, et le doublon masquerait un oubli d'abonnement.
|
|
|
|
**Sans profil actif** (`DEFAULT_WMS_PROFILE` absent ou invalide), `getCurrent()`
|
|
lève une erreur qui **énumère les profils disponibles**. C'est intentionnel :
|
|
Claude lit ce message et enchaîne sur `switch_wms_profile` au lieu d'échouer.
|
|
|
|
---
|
|
|
|
## D9 — Profils SaaS : accès aux logs refusé, pas silencieux
|
|
|
|
**Décision.** Quand `<PROFIL>_SAAS=true`, `read_recent_logs`, `search_logs` et
|
|
`list_log_files` **lèvent une erreur explicite** renvoyant vers les outils API.
|
|
|
|
**Pourquoi.** Le WMS est hébergé dans le cloud Mecalux : le partage
|
|
`\\<host>\inetpub\logs\...` n'est pas joignable. Retourner « 0 fichier » ferait
|
|
croire à une absence d'erreurs dans les logs, ce qui est un faux négatif
|
|
dangereux en diagnostic. Voir [docs/logs.md](docs/logs.md).
|
|
|
|
---
|
|
|
|
## D10 — Chargement paresseux + cache 1 h
|
|
|
|
**Décision.** Workflows et éléments AD ne sont **pas** chargés au démarrage,
|
|
mais à la première requête qui les concerne, puis mis en cache
|
|
(`WORKFLOW_CACHE_TTL`, 3 600 000 ms par défaut).
|
|
|
|
**Pourquoi.** L'ensemble représente ~38 800 éléments dont 29 374 `Resource` :
|
|
tout charger au boot ferait échouer le handshake MCP par timeout, pour des
|
|
données souvent inutiles à la session.
|
|
|
|
**Pagination.** La taille de page est réglée **par type** dans
|
|
`AD_ELEMENT_TYPES` (`src/services/ad-service.js`) : `View: 200`,
|
|
`Workflow: 5000`, `Resource: 15000`, tout le reste `100000` (soit une seule
|
|
page). Ces valeurs viennent de l'observation des timeouts serveur — les
|
|
augmenter à l'aveugle fait échouer les types lourds.
|
|
|
|
---
|
|
|
|
## D11 — Les paramètres système : `Parameter` + `ParamValue`, fusionnés en JS
|
|
|
|
**Piège.** Le modèle Reading ne contient **pas** d'entité
|
|
`CommandParameterData`. La configuration se lit dans deux entités :
|
|
|
|
| Entité | Contenu |
|
|
|---|---|
|
|
| `Parameter` | définition + `DefaultValue` |
|
|
| `ParamValue` | surcharges par entrepôt, liées par `ParameterId` |
|
|
|
|
**Décision.** `get_system_parameters` charge les deux intégralement (~200 et
|
|
~50 lignes) et fait la fusion **côté JavaScript**, en exposant la *valeur
|
|
effective* par entrepôt (surcharge si présente, défaut sinon).
|
|
|
|
**Pourquoi côté JS.** Les filtres (`warehouse`, `param_class`, `search`,
|
|
`only_overridden`) sont appliqués en JS pour éviter toute concaténation de
|
|
chaîne LINQ à partir d'entrées utilisateur — pas d'injection possible, et pas
|
|
de dépendance aux limites du traducteur LINQ (D12).
|
|
|
|
Fichier : `src/tools/config-tools.js`.
|
|
|
|
---
|
|
|
|
## D12 — Les dates relatives ne sont pas traduisibles en LINQ
|
|
|
|
**Piège vérifié en production.** `DateTime.Now`, `DateTime.Today` et
|
|
`AddDays()` ne sont **pas** traduits par le moteur de requêtes : la requête
|
|
échoue à la compilation.
|
|
|
|
**Solution.** Toujours une date littérale :
|
|
|
|
```csharp
|
|
Context.OutboundOrders.Where(z => z.CreationDate > new DateTime(2026, 8, 1))
|
|
```
|
|
|
|
C'est à l'appelant (donc à Claude) de calculer la date avant d'écrire la
|
|
requête.
|
|
|
|
---
|
|
|
|
## D13 — `select_expression` reste instable
|
|
|
|
**État.** Les projections passées via le paramètre API `Select` déclenchent des
|
|
erreurs de compilation côté serveur.
|
|
|
|
**Contournement actuel.** Interroger les lignes complètes et filtrer les
|
|
colonnes côté client.
|
|
|
|
**Non résolu.** C'est le principal point ouvert du projet. Toute tentative de
|
|
correction doit être validée sur un vrai WMS avant d'être documentée ici.
|
|
|
|
---
|
|
|
|
## D14 — `executeCommand` : pas de suffixe d'assembly
|
|
|
|
**Piège.** Ajouter `, Mecalux.ITSW.EasyWMS.Modules.Contracts` au nom de commande
|
|
provoque une `FileLoadException`.
|
|
|
|
**Solution.** Utiliser le `command_name` **tel quel** :
|
|
l'`InternalCommandName` fourni par l'AD contient déjà le nom pleinement
|
|
qualifié correct.
|
|
|
|
---
|
|
|
|
## D15 — Validation TLS désactivée
|
|
|
|
**Décision.** `httpsAgent: new https.Agent({ rejectUnauthorized: false })`.
|
|
|
|
**Pourquoi.** Les WMS on-premise sont exposés en HTTPS avec un certificat
|
|
auto-signé sur une IP privée.
|
|
|
|
**Limite assumée.** Acceptable sur réseau interne ou via VPN. Sur un profil
|
|
SaaS joint par Internet, cela supprime la protection contre l'interception —
|
|
à revoir si l'outil sort du cadre du diagnostic interne.
|
|
|
|
---
|
|
|
|
## D16 — Historique des shipment templates : hors périmètre
|
|
|
|
**Constat.** L'entité Reading `ShipmentTemplate` n'expose que la **dernière**
|
|
exécution (`LastExecuteDate`, `Status`, `IsEnabled`).
|
|
|
|
L'historique complet n'existe que dans les logs texte
|
|
`ApplyShipmentTemplates`, **absents de l'hôte joignable** (`10.255.255.2`) :
|
|
ils résident sur les serveurs de production / ETL des clients.
|
|
|
|
**Décision.** Aucun outil d'analyse de ces logs n'a été construit — il n'aurait
|
|
rien à lire. Les recettes purement API sont dans la resource
|
|
`wms://query-examples`.
|
|
|
|
---
|
|
|
|
## D17 — `WorkflowAction` et `WritingModel` retirés de la liste AD
|
|
|
|
**Constat.** Les endpoints `/AD/api/WorkflowAction/GetByApplication` et
|
|
`/AD/api/WritingModel/GetByApplication` répondent `404 Not Found`.
|
|
|
|
**Décision.** Ces deux types sont sortis de `AD_ELEMENT_TYPES` : il en reste
|
|
**20**, dont 3 valides mais vides (`Dashboard`, `TimelineTemplate`, `Toggle`).
|
|
Détail de la campagne de validation :
|
|
[docs/ad-api-validation.md](docs/ad-api-validation.md).
|
|
|
|
---
|
|
|
|
## D18 — Build : `@yao-pkg/pkg` ciblant node22
|
|
|
|
**Décision.** Le build utilise `@yao-pkg/pkg` (fork maintenu de `pkg`, archivé
|
|
depuis) avec la cible **`node22-win-x64`**.
|
|
|
|
**Pourquoi cette cible.** `node20-win-x64` n'a pas de binaire prébuilt
|
|
disponible : pkg bascule alors sur une compilation de Node depuis les sources,
|
|
qui échoue faute de `vcbuild.bat` (toolchain MSVC absente).
|
|
|
|
**Avertissements normaux au build.** `Cannot find module
|
|
'@modelcontextprotocol/sdk/server/index.js'` et `Entry 'main' not found` :
|
|
pkg ne sait pas résoudre statiquement la table `exports` du SDK. L'exécutable
|
|
produit **fonctionne** — vérifié en démarrant l'exe. Ne pas chercher à
|
|
« corriger » ces avertissements.
|
|
|
|
---
|
|
|
|
## D19 — La resource `docs://` a été supprimée
|
|
|
|
**Constat.** `src/resources/documentation.js` (193 lignes) exposait un index des
|
|
`.md` de `docs/`, mais n'a **jamais été branché** dans `src/index.js` : le
|
|
handler `resources/list` ne l'incluait pas et `resources/read` ne routait aucune
|
|
URI `docs://`. `docs/README.md` promettait pourtant la fonctionnalité aux
|
|
utilisateurs.
|
|
|
|
**Décision.** Fichier supprimé, `docs/README.md` corrigé. `docs/` reste un
|
|
dossier de référence pour les humains et pour un agent qui lit le dépôt — pas
|
|
une resource MCP.
|
|
|
|
**Si on veut la fonctionnalité un jour**, il faut la brancher réellement (2
|
|
lignes dans `src/index.js`) *et* décider de son sort dans l'exécutable pkg, qui
|
|
n'embarque pas `docs/`.
|
|
|
|
---
|
|
|
|
## D20 — Purge du dépôt (2026-08-24)
|
|
|
|
Supprimés lors du nettoyage :
|
|
|
|
| Élément | Raison |
|
|
|---|---|
|
|
| 5 `.md` dupliqués à la racine | copies md5-identiques de `docs/api/` et `docs/entities/` |
|
|
| `JANITOR_main.js`, `JANITOR_entities.json` | application Electron sans lien avec le MCP |
|
|
| `temp/*.json` | dumps de workflows versionnés par accident (`temp/` désormais ignoré) |
|
|
| `claude_desktop_config_ssh.json` | **mots de passe en clair** + variables `ORACLE_*` de l'architecture supprimée (D1) |
|
|
| `IMPLEMENTATION_SUMMARY.md` | doublon d'`AD_API_TEST_RESULTS.md`, déplacé en `docs/ad-api-validation.md` |
|
|
| `src/resources/documentation.js` | code mort (D19) |
|
|
| `src/config/constants.js` (83 l.) | module entier inutilisé : `require` présent dans `wms-query-service.js`, mais **aucune** de ses constantes n'était lue |
|
|
| `log-service.js` : `findRecentErrors`, `readFullLog`, `getLogStats` (~100 l.) | exportées, jamais appelées — aucun outil MCP ne les exposait |
|
|
| `RESOURCE_URIS.WORKFLOWS_CATEGORIES` | URI déclarée, jamais servie |
|
|
| `LOG_FILE_PATTERN` | lue depuis `.env`, jamais utilisée (le scan filtre sur `.log` en dur) |
|
|
|
|
Les trois fonctions de `log-service.js` étaient fonctionnelles ; si l'une d'elles
|
|
redevient utile (`findRecentErrors` en particulier), la reprendre depuis le
|
|
commit `b59cbb3` et **l'exposer réellement** comme outil MCP plutôt que de la
|
|
laisser inatteignable.
|
|
|
|
⚠️ **Credentials à faire tourner.** `claude_desktop_config_ssh.json` et
|
|
l'ancienne version de `test-ad-api.ps1` contenaient des mots de passe en clair.
|
|
Le fichier est retiré du répertoire de travail, **mais il reste dans
|
|
l'historique git** (commit `b59cbb3`). Considérez ces mots de passe comme
|
|
compromis et changez-les ; à défaut, réécrivez l'historique avant toute
|
|
publication du dépôt.
|
|
|
|
---
|
|
|
|
## D21 — `Context.{...}` attend le `TableName` du Metadata, résolu par service
|
|
|
|
**Piège.** Les expressions LINQ de `QueryExecute` référencent les entités par
|
|
le `TableName` de l'API Metadata, **pas** par le nom d'entité de l'Application
|
|
Dictionary. Ce n'est pas une pluralisation : `Container` -> `Containers`, mais
|
|
`Alias` -> `Alias` (invariant), et `Item` n'existe pas. Un nom faux part en
|
|
HTTP 500 (erreur de compilation `'ApplicationReadingContext' ne contient pas
|
|
de définition pour '...'`). Seul `TableName` fait foi — **ne réinventez pas de
|
|
règle grammaticale**.
|
|
|
|
**Décision.** `src/services/entity-resolver.js` construit une table
|
|
`Name | TableName (insensible à la casse) -> TableName` et tous les points
|
|
d'interpolation (`wms-query-service`, `call_query_api`) passent par elle.
|
|
Mesures du 24/08/2026 (`LIMAGRAI2512`) :
|
|
|
|
- Le contexte de lecture est **commun au tenant** : la table agrège le
|
|
Metadata de toutes les applications. La liste vient de
|
|
`GET /configuration/applications` (5 applications déployées avec version) —
|
|
les applications EasyBuilder sans contexte requêtable (`CustomApp`…) n'y
|
|
figurent pas et ne fournissent de toute façon **0 entité** Metadata.
|
|
- 288 `TableName` distincts, aucun conflit `Name -> TableName` entre
|
|
applications.
|
|
|
|
Comportements :
|
|
|
|
- **Nom inconnu** : échec avant tout appel réseau de requête, message avec
|
|
suggestions proches et renvoi vers `get_entity_metadata`.
|
|
- **Metadata injoignable** : le nom passe tel quel (comportement historique)
|
|
et la réponse porte un `warning` — on ne bloque pas tout le serveur pour un
|
|
cache irrécupérable.
|
|
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
|
|
invalidation par abonnement `onSwitch()` (D8, D10).
|
|
|
|
---
|
|
|
|
## D22 — Routage des outils par table explicite, plus par préfixe de nom
|
|
|
|
**Piège.** Le handler `tools/call` de `src/index.js` routait par préfixe de nom
|
|
(`startsWith`, `includes`) dans une cascade de `else if`. Deux outils listés
|
|
dans `tools/list` n'atteignaient jamais leur module — reproduits le
|
|
24/08/2026 :
|
|
|
|
| Outil | Cause | Erreur renvoyée |
|
|
|---|---|---|
|
|
| `get_entity_metadata` | capté par `startsWith('get_entity_')` (branche `wms-query-tools`, placée avant la sienne) | `Unknown WMS query tool: get_entity_metadata` |
|
|
| `list_log_files` | la branche logs testait `includes('_logs')`, or le nom contient `_log_files` | `Unknown tool: list_log_files` |
|
|
|
|
Le routage par préfixe fait dépendre la joignabilité d'un outil de l'**ordre
|
|
des branches** et de conventions de nommage implicites : chaque ajout d'outil
|
|
pouvait en casser un autre silencieusement.
|
|
|
|
**Décision.** Une table `nom d'outil → module` est construite au démarrage en
|
|
parcourant les `listTools()` des 8 modules de `src/tools/`. `tools/list` est
|
|
servi depuis cette même table et le dispatch est un lookup : un outil listé
|
|
est un outil routé, **par construction**. Deux modules déclarant le même nom
|
|
font échouer le serveur au démarrage (message nommant les deux modules) —
|
|
c'est un bug de développement, pas un cas d'exécution.
|
|
|
|
La table ne présume rien de la signature des outils : `(name, args)` est
|
|
transmis tel quel au `executeTool()` du module. Ajouter un paramètre à un
|
|
outil ne la concerne pas.
|