Français
Conventions
La plupart des règles du dépôt sont appliquées par l’outillage : le compilateur TypeScript, ESLint (eslint.config.js), Biome (biome.json) et des tests qui analysent le code. Quand une règle ne peut pas l’être, elle doit se vérifier en quelques secondes en revue. Cette page liste les règles et l’endroit où chacune est vérifiée.
Langue du code
Les identifiants, les noms de tables et de colonnes et les messages d’erreur techniques sont en anglais. Tout texte qu’un utilisateur peut lire passe par les traductions, en français et en anglais (voir Textes et traductions).
Un concept, un mot, partout : le code, la base, l’API et l’interface emploient les mêmes identifiants (member, mailbox, workflow, execution, step, scope, template…). N’introduisez pas de synonymes.
TypeScript
Les réglages partagés sont dans tsconfig.base.json :
strict, plusnoUncheckedIndexedAccessetexactOptionalPropertyTypes: un accès indexé peut valoirundefined, et une propriété facultative ne reçoit pas unundefinedexplicite sauf si son type le prévoit ;noImplicitReturns,noImplicitOverride,noFallthroughCasesInSwitch,noUnusedLocals,noUnusedParameters,useUnknownInCatchVariables;verbatimModuleSyntax,erasableSyntaxOnly(pas d’enum, pas de namespace : utilisez des objetsas const),isolatedModules;- des modules ES purs (
"type": "module"), des imports relatifs avec leur extension.ts.
ESLint ajoute, en erreur :
| Règle | Effet |
|---|---|
@typescript-eslint/no-explicit-any | any est interdit. Utilisez unknown et affinez ; validez les données externes avec zod. |
@typescript-eslint/no-floating-promises, no-misused-promises | Toute promesse est attendue ou explicitement gérée. Un « lancer sans attendre » assumé s’écrit void promesse. |
@typescript-eslint/consistent-type-imports | Les imports de types passent par import type ou type en ligne. |
@typescript-eslint/no-unused-vars | Les noms inutilisés sont des erreurs, sauf ceux qui commencent par _. |
no-empty (blocs catch compris) | Pas de catch muet : un catch traite l’erreur, ou l’enrichit et la relance. |
no-console | Seuls console.warn et console.error. Utilisez le journal. |
Préférez les unions discriminées à plusieurs booléens (status: 'queued' | 'running' | …).
Frontières entre paquets
Les dépendances entre paquets sont strictement descendantes. eslint.config.js déclare, pour chaque paquet, la liste exhaustive des paquets @manko/* qu’il peut importer ; tout autre import est une erreur.
| Paquet | Peut importer |
|---|---|
workflow | rien du produit |
api-types | workflow |
credentials | workflow |
nodes | workflow |
connectors | workflow, credentials |
mirror | workflow, connectors |
engine | workflow, nodes |
tools | workflow |
server | tous les paquets ci-dessus |
ui | workflow, api-types, nodes |
apps/docs | workflow, api-types, nodes |
ESLint refuse aussi :
- les imports profonds dans un autre paquet (
@manko/<paquet>/<chemin>) : un paquet n’expose que son point d’entrée ; - l’accès à un autre paquet par chemin relatif (
../../autre/src/…) ; - toute dépendance à un paquet de l’écosystème n8n (
n8n,n8n-*,@n8n/*). Copier du code n8n est également interdit, grammaires, chaînes de traduction et schémas compris.
Un nœud n’importe jamais un connecteur ni le SDK d’un fournisseur : les effets externes lui arrivent par les services de son contexte. Le même principe vaut ailleurs : la logique métier reçoit des interfaces (envoi de mail, service LLM, client HTTP, horloge, file) par injection, ce qui rend possibles les essais et les tests sans réseau.
Configuration et variables d’environnement
Seul le module de configuration typée, packages/server/src/config, lit process.env. ESLint refuse process.env partout ailleurs dans le code du produit. Les fichiers de test et le générateur de documentation en sont dispensés. Chaque variable est documentée dans .env.example et dans Variables d’environnement.
Bibliothèques cantonnées à un module
Certaines bibliothèques s’importent depuis un seul module, et ESLint les refuse ailleurs :
| Bibliothèque | Seul module autorisé |
|---|---|
dompurify, jsdom | packages/server/src/webmail-read/sanitize.ts (serveur), packages/ui/src/features/webmail/model/emailBody.ts (interface) |
css-tree | packages/server/src/webmail-read/css.ts |
Tout nouveau chemin d’affichage d’un contenu externe (corps de mail) passe par ces modules d’assainissement.
Formatage
Biome est le seul formateur. pnpm format reformate, pnpm lint vérifie. Les réglages (biome.json) : espaces, indentation de 2, largeur de ligne de 100, fins de ligne LF, guillemets simples, points-virgules, virgules finales partout, parenthèses autour des paramètres de fonctions fléchées, imports organisés automatiquement. Le linter propre à Biome est désactivé : les règles de lint viennent d’ESLint. Dans l’interface, ESLint n’emploie que les règles essential du plugin Vue, pour ne jamais entrer en conflit avec Biome.
Le style ne se discute jamais en revue : le formateur tranche.
Erreurs et nouvelles tentatives
La distinction entre erreurs transitoires et définitives pilote les nouvelles tentatives : choisissez-la explicitement à chaque throw sur un appel externe.
| Où | Classes |
|---|---|
Nœuds (packages/nodes/src/errors.ts) | PermanentNodeError, RetryableNodeError, avec un code stable, un message en anglais, cause et details. |
Moteur (packages/engine/src/errors.ts) | TransientError, PermanentError, RateLimitedError (reprogrammée sans consommer de tentative). |
Le moteur lit la classification par la propriété failureKind ; une erreur non classée est traitée comme transitoire. Chaînez l’erreur d’origine par cause, et ne mettez jamais le contenu d’un mail ni un secret dans details ou dans les messages.
Format d’erreur de l’API
Toutes les erreurs de l’API ont la même forme, définie par apiErrorSchema dans packages/api-types/src/errors.ts :
json
{ "code": "workflow.graph_outdated", "message": "technical message in English", "details": {} }codeest stable et fait partie du contrat : minuscules, chiffres,_et.(^[a-z][a-z0-9_.]*$). Il ne se renomme jamais sans nouvelle version de l’API.messageest technique, en anglais, pour les journaux et le débogage. Le serveur n’envoie aucun texte d’interface.detailsest un contexte structuré facultatif.
L’interface traduit le code : le message lisible vit sous errors.<code> dans packages/ui/src/i18n/locales/fr.json et en.json, avec un repli générique. Chaque corps de requête et de réponse a un schéma zod dans @manko/api-types, partagé par le serveur (validation) et l’interface (types).
Textes et traductions
Aucun texte en dur, et le français et l’anglais livrés ensemble, dans la même modification.
| Texte | Où il va |
|---|---|
Interface (libellés, placeholders, title, aria-label, messages d’erreur) | packages/ui/src/i18n/locales/fr.json et en.json |
| Libellés, descriptions et options des nœuds | Dans la définition du nœud, en { fr, en } |
| Résumés d’étape écrits par les nœuds pendant l’exécution | packages/nodes/src/i18n/fr.ts et en.ts |
| Textes que le serveur écrit pour une personne (mail d’approbation, page de confirmation, résumé quotidien) | packages/server/src/i18n/fr.ts et en.ts |
| Textes d’une intégration (nom, guide, champs) | Dans la déclaration de l’intégration, en { fr, en } |
Syntaxe des messages : {nom} interpole, et {compteur|singulier|pluriel} accorde selon la règle de pluriel de la langue (« 0 fichier » en français, « 0 files » en anglais). Écrivez une clé par phrase entière, jamais deux moitiés recollées ; une concaténation devient une clé paramétrée. Dates, durées, nombres et tailles se formatent avec Intl et la langue courante.
Des tests le font respecter :
packages/ui/src/i18n/hardcoded.test.tsanalyse les sources de l’interface et échoue sur un texte de gabarit, un attribut non lié (title="…") ou un littéral français dans le code ;packages/ui/src/i18n/i18n.test.tsvérifie quefr.jsoneten.jsonont exactement les mêmes clés, et refuse une valeur anglaise identique à la française hors d’une liste d’exceptions (noms propres, codes) ;packages/nodes/src/i18n/hardcoded.test.ts,packages/nodes/src/i18n/i18n.test.tsetpackages/server/src/i18n/i18n.test.tsfont de même pour les textes des nœuds et du serveur.
Les dispenses de fichier entier de l’analyse de l’interface sont réservées aux fichiers qui portent déjà les deux langues dans un catalogue local vérifié par le typeur (par exemple les messages des formulaires de nœud) ; une chaîne française sans son équivalent anglais n’est jamais dispensée.
Les fichiers de traduction sont partagés par de nombreuses modifications : ajoutez vos clés sous votre propre espace de noms et modifiez les fichiers, ne les réécrivez jamais en entier.
Les prompts envoyés aux modèles de langage s’écrivent en anglais dans le code. Ce sont des consignes données au modèle, pas du texte d’interface.
Nom du produit et domaines
Le nom affiché du produit vient de la variable de configuration BRAND_NAME, et l’adresse de l’instance de PUBLIC_BASE_URL. N’écrivez jamais le nom du produit ni un domaine dans le code. La documentation écrit le jeton Mankomail, remplacé au build, et son générateur refuse toute page qui contient le nom de code du produit.
Base de données et migrations
- Les requêtes passent par Kysely, uniquement dans les repositories (un par agrégat, méthodes nommées d’après les cas d’usage). Les gabarits SQL bruts sont réservés aux repositories.
- Les migrations sont des fichiers SQL bruts dans
packages/server/migrations, nommésNNNN_nom.sqlet appliqués dans l’ordre des numéros par l’exécuteur de migrations du serveur, sous un verrou consultatif. Une migration n’est jamais modifiée une fois fusionnée : son empreinte SHA-256 est stockée et vérifiée. Il n’existe pas de migration descendante ; une migration doit rester compatible avec la version précédente du code (étendre, puis resserrer). - Toute écriture sur plusieurs tables passe par une transaction explicite. Les invariants critiques sont portés par le schéma (contraintes d’unicité, clés étrangères), pas par le code.
- Tout effet externe et toute création d’exécution passent par leur clé unique. Un conflit d’unicité sur cette clé est un résultat normal (sans effet), pas une erreur.
Règles de sécurité
- Les secrets et les credentials ne sont jamais journalisés (le journal masque les chemins connus), jamais placés dans les données d’étape, jamais placés dans une URL.
- Les jetons aléatoires sont stockés sous forme d’empreinte calculée par
hashToken, et les secrets se comparent avecconstantTimeEquals, tous deux danspackages/server/src/security/tokens.ts. Ne les réimplémentez pas. - Le contenu d’un mail n’atteint un modèle de langage que par le service LLM, et les prompts séparent les données des consignes.
Nœuds
Un nœud suit le contrat defineNode et rien d’autre : pas d’accès direct à la base, pas d’import de connecteur, uniquement les services de son contexte. Sa policy.effect est honnête, tout effet externe reçoit context.idempotencyKey, tout appel réseau respecte context.abortSignal. Le catalogue garde une seule version de chaque nœud, modifiée sur place. Le détail est dans Écrire un nœud.
Commits et pull requests
- Les messages de commit suivent la forme Conventional Commits :
type: sujetoutype(portée): sujet, avec des types commefeat,fix,refactor,docs,test. Aucun hook de commit ni linter de commit n’est installé : la forme se vérifie en revue. - Gardez des branches courtes et des pull requests petites (moins de 400 lignes environ hors tests et fichiers générés) ; découpez les modifications plus grandes.
- Un bogue corrigé s’accompagne d’un test qui le reproduit, dans la même modification.
- Une modification d’un comportement documenté met à jour la documentation dans la même modification, y compris ce site (sources des nœuds, guides d’intégration, pages).
- Avant d’ouvrir une pull request, lancez
pnpm typecheck,pnpm lintet les tests concernés. La CI exécute ensuite la vérification des types, le lint, la suite de tests complète avec PostgreSQL et MinIO, le build et un test fumigatoire de la pile en conteneurs (voir Contribuer). - La revue vérifie d’abord les invariants : idempotence, transactions, périmètre, effets déclarés.