Architecture
PICKET est un monolithe modulaire dans un workspace pnpm : une image, plusieurs réplicas identiques, PostgreSQL comme unique source de vérité.
text
apps/bot racine de composition : serveur HTTP, runner Gateway, ligne de commande
packages/kernel identifiants, Result, Clock, Logger, Secret
packages/config validation de l'environnement
packages/i18n traductions, résolution de langue, contrôle des catalogues
packages/persistence accès à la base, migrations, sécurité par ligne
packages/coordination baux avec fencing, verrous par clé, stockage des sessions Gateway
packages/discord pipeline d'interactions, registres de commandes et de composants, adaptateurs HTTP et Gateway, ports REST
packages/guild serveurs : réglages, permissions, cycle de vie
packages/todolist todolists : grammaire, rendu, cochage (l'état vit dans le message Discord)
packages/testing outils partagés par les testsCouches
Chaque package fonctionnel est découpé en domain/, application/, infrastructure/ et presentation/. Les dépendances ne vont que vers l'intérieur. Un test (tests/architecture) fait échouer le build quand domain/ ou application/ importe Discord, la base, HTTP ou des modules Node, quand un package va chercher dans un autre au lieu de son API publique, ou quand du code hors de @picket/config lit process.env.
Règles à connaître
- Un serveur est un tenant. Toute table qui contient des données de serveur a une colonne
guild_idet une sécurité par ligne qui litapp.guild_id; le code y accède parwithTenant. Un test échoue si une telle table n'a pas la politique. L'application se connecte avec un rôle qui ne possède rien et ne peut pas contourner la politique. - Idempotence. Les interactions sont réclamées une seule fois entre les réplicas ; les écritures utilisent des clés naturelles et des mises à jour conditionnelles.
- Les singletons utilisent des baux. Un travail qui ne doit tourner qu'une fois (un shard Gateway) est tenu par un bail avec jeton de fencing, vérifié par les écritures du détenteur.
- Mises à jour sans interruption. Les migrations ne font qu'ajouter ; les charges utiles et identifiants qui traversent les versions sont versionnés.
- Les commandes sont déclarées une fois. Le registre valide noms, options et textes au démarrage et génère le JSON envoyé à Discord. Chaque commande déclare explicitement son niveau requis.
Ajouter une commande
- Ajoutez les textes dans
packages/i18n/locales/en.jsonetfr.json, puis lancezpnpm i18n:keys. - Écrivez le cas d'usage dans la couche
application/, avec un port pour ce dont il a besoin. - Ajoutez l'adaptateur dans
infrastructure/et la commande danspresentation/discord/(niveau, options, handler utilisantt). - Enregistrez-la dans
apps/bot/src/composition.ts. - Testez-la : tests unitaires avec des doublures en mémoire, et un test d'intégration sur PostgreSQL si elle touche aux données.
Ajouter un bouton ou un formulaire
- Déclarez une
ComponentFamily: un espace de noms (td), une version, le niveau requis, et la fonctionnalité du serveur dont elle dépend. - Construisez les identifiants avec
encodeCustomId(espace:version:charge, 100 caractères au plus). Un nouveau format demande une nouvelle version : les anciens boutons reçoivent une réponse « expiré », jamais le silence. - Enregistrez la famille dans
apps/bot/src/composition.ts. Boutons et formulaires passent par la même chaîne que les commandes : doublons, niveau d'accès, fonctionnalité, suspension, langue, erreurs. - Renvoyez
{ kind: 'deferred', ... }pour tout ce qui parle à Discord : le pipeline accuse réception sous 3 secondes, exécute le travail ensuite et livre le résultat ; l'arrêt d'une réplique l'attend.
Tests
sh
pnpm test # typecheck + tests unitaires (rapide, sans Docker)
pnpm test:int # tests d'intégration sur un PostgreSQL jetable (nécessite Docker)
pnpm test:all # les deuxLa chaîne d'outils est TypeScript 7 (tsc), Jest avec @swc/jest, et pas de linter : le test d'architecture et le mode strict du compilateur font ce travail.