Chapitres
Sur cette page
DOC-04 / Référence technique · Chapitre 02
Couche données de Synedre OS
Ce chapitre décrit l'architecture data du harness Synedre OS : base PostgreSQL unique `ac_hub`, trois chemins d'accès (Nuxt, Python agentique, Drizzle ORM), familles de tables par préfixe et conventions de nommage reliant CodeMyShop et le cockpit agentique.
La couche données
Cette page décrit comment le harness agentique de Synedre OS lit et écrit ses données : une base PostgreSQL centrale, un adaptateur qui traduit le SQL legacy MySQL vers PostgreSQL côté interface web, des classes d'entités Python côté outillage agentique, un ORM utilisé comme schema-as-code pour le DDL et les migrations, et enfin les conventions de nommage — préfixes de tables, internationalisation, polymorphisme — qui font tenir le tout. Elle est destinée aux ingénieurs qui reprennent le code.
1. La base de données centrale
Toute la donnée du vaisseau-mère vit dans un seul schéma PostgreSQL, hébergé dans un conteneur dédié sur le VPS du vaisseau-mère.
| Élément | Description |
|---|---|
| Conteneur | Le conteneur PostgreSQL du vaisseau-mère |
| Base | La base principale (distincte du nom du conteneur) |
| Schéma | Le schéma central du harness |
| Utilisateur DB | Un utilisateur dédié, défini par variable d'environnement |
| Mot de passe | Porté par le fichier .env / .env.host, jamais en clair — une exception est levée si absent |
Attention au piège récurrent : le conteneur et la base portent des noms distincts. Les tables du harness vivent dans le schéma central, pas dans le schéma public.
Frontière harness / multi-tenant
Ce chapitre documente exclusivement le schéma central du harness Synedre OS (mono-base, cockpit agentique privé). Les bases de données des tenants CodeMyShop utilisent le même adaptateur PostgreSQL (voir §4–5), mais leurs données ne croisent jamais le schéma du harness : chaque tenant dispose de sa propre base, de son propre schéma, et de ses propres tables. Le §4 couvre l'adaptateur car il sert aussi au cockpit — mais toute référence aux tenants y est contextuelle, pas constitutive du harness.
Trois chemins d'accès
Deux chemins opèrent à l'exécution (DML : lire/écrire des lignes) ; un troisième intervient à la conception (DDL : faire évoluer la structure) :
┌──────────────────────────────────┐
│ PostgreSQL — base centrale │
│ schéma du harness │
└───────────────┬──────────────────┘
│
┌─────────────────┬─────────────┴──────────┬─────────────────────┐
│ DML runtime │ DML outillage │ DDL / structure │
│ Interface web │ Python agentique │ Schema-as-code ORM │
│ pool TCP │ exécution SQL via │ génération + │
│ (postgres-js) │ conteneur DB │ migrations SQL │
└─────────────────┴─────────────────────────┴─────────────────────┘
Les deux chemins DML s'adressent au même schéma mais via des transports différents :
- Côté interface web : un pool de connexions TCP (bibliothèque
postgres-js). - Côté outillage Python : exécution SQL via le conteneur — les écritures passent par un fichier
.sqltemporaire copié puis exécuté ; les lectures utilisent une commande SQL inline. Aucun chemin ne passe par un pipe stdin.
Le troisième chemin ne manipule pas de lignes : c'est le DDL en schema-as-code (ORM Drizzle) qui fait foi pour l'évolution structurelle des tables — détaillé en §6.
État des familles de tables (instantané 2026-05-31)
| Famille | Tables | Périmètre |
|---|---|---|
Tables legacy privées (préfixe ps_ac_) |
159 | Legacy CodeMyShop / cockpit (dette à migrer) |
Tables cockpit Synedre OS (préfixe sy_) |
69 | Agents, chantiers, runs, négociations… |
Tables PrestaShop natif (préfixe ps_) |
18 | Dette historique : produits, catégories, traductions, langues… |
Tables PaaS public OSS (préfixe cs_) |
1 | Voir note ci-dessous |
À noter concernant les tables PaaS public :
- La table qui journalisait les erreurs du cockpit a migré vers la famille cockpit (
sy_) : elle enregistre les erreurs du cockpit, pas du produit. - En sens inverse, la table de file d'attente SEO en masse a intégré la famille PaaS (
cs_) — enfilée par le back-office CodeMyShop, elle vit surtout dans les bases tenants ; sa présence ici est un vestige de sa création (table vide sur la base centrale, aucun écrivain actif).
S'y ajoutent 16 vues dans le schéma central (voir §3.4 — point critique : la vue des agents repose sur la table cockpit correspondante).
⚠️ Le décompte exact évolue à chaque chantier ; les chiffres ci-dessus sont un instantané. La requête de vérité est donnée en §7.
2. Les familles de tables (préfixes)
Le préfixe d'une table encode son périmètre et son régime de propriété.
| Préfixe | Périmètre | Statut | Exemples représentatifs |
|---|---|---|---|
cs_ |
PaaS public OSS (core + communauté) | Cible | FAQ, blocs homepage (dans les bases tenants) |
sy_ |
Cockpit interne agentique Synedre OS | Cible | Tâches de chantier, agents, runs, négociations |
ps_ac_ |
Legacy privé CodeMyShop / cockpit | Dette à migrer | Chantiers, cicatrices, VPS clients |
ps_ |
PrestaShop natif | Dette historique | Produits, catégories, traductions, langues |
Conventions transverses
- Nom singulier (
cs_faq, pascs_faqs) — aligné sur la convention PrestaShop natif. - Une table = une entité parente ; pas de table fourre-tout.
- Fichiers source en kebab-case ; composants Vue en PascalCase.
Historique de migration des préfixes
Le préfixe cs_ résulte d'un rename unique effectué lors du passage en version 0.2.0, depuis l'ancien préfixe ps_ac_ (à l'époque où CodeMyShop était une extension PrestaShop). Dans le schéma du vaisseau-mère, la plupart des tables sont restées en ps_ac_ (la dette n'a pas encore été soldée), tandis que le préfixe cs_ est appliqué dans les bases tenants OSS.
2.1 Cohabitation des préfixes dans une même entité logique
Point qui surprend à la reprise : une seule entité logique « chantier » est répartie sur deux familles de préfixes — legacy ET cockpit.
| Table | Famille | Rôle |
|---|---|---|
| Table principale du chantier | Legacy (ps_ac_) |
Une ligne par chantier |
| Table des travaux granulaires | Legacy (ps_ac_) |
N travaux par chantier |
| Table des tâches atomiques | Cockpit (sy_) |
N tâches par travail |
| Satellites cockpit (équipe, verrou multi-session, QA…) | Cockpit (sy_) |
Données d'orchestration et de contrôle |
Détail révélateur de l'historique : la table des tâches atomiques a été renommée au niveau de son nom visible, mais sa séquence de clé primaire et le nom de sa contrainte PRIMARY KEY conservent l'ancien préfixe. Le rename a touché le nom de la table, pas tous les objets dépendants — à prendre en compte lors d'inspections du catalogue PostgreSQL.
Modèle de données des tables principales
La table des chantiers
La table centrale des chantiers repose sur une clé primaire auto-incrémentée. Voici ses colonnes notables :
| Colonne | Type | Rôle |
|---|---|---|
codename |
varchar(64) NOT NULL | Identifiant kebab-case unique du chantier |
title |
varchar(255) NOT NULL | Intitulé lisible |
client_id |
varchar(32) | Référence au tenant concerné ; NULL = chantier interne |
status |
varchar(9), défaut 'planning' |
État courant du chantier |
priority |
varchar(2), défaut 'P2' |
Niveau de priorité |
current_focus |
text | Objectif immédiat en cours |
deadline |
date | Échéance cible |
notes / mission_letter / preprod_test_plan |
text | Documentation libre, lettre de mission, plan de recette |
external_contacts |
text | Contacts externes liés au chantier |
ship_command |
varchar(255) | Commande de livraison associée |
scope |
varchar(32) | Périmètre fonctionnel (enum contraint) |
auto_explode |
boolean, défaut true |
Décomposition automatique en tâches |
mode_auto |
boolean, défaut false |
Pilotage entièrement automatisé |
max_cost_eur |
numeric(8,2) | Enveloppe budgétaire maximale |
archived_at / archived_by |
— | Horodatage et auteur de l'archivage |
date_add / date_upd |
timestamptz, défaut now() |
Dates de création et de dernière modification |
La colonne scope est contrainte par un CHECK dont l'énumération couvre notamment les périmètres synedre, codemyshop-oss, codemyshop-enterprise, tenant, business, juridique, negociation et conseil. Notons que les valeurs juridique, negociation et conseil existent bien dans la base de données en production mais ne figurent pas dans tous les documents de référence internes — il convient de s'appuyer sur l'introspection de la base live plutôt que sur la documentation statique.
Un déclencheur (trigger) s'exécute après toute mise à jour du statut : lorsqu'un chantier passe à l'état terminé, il propage automatiquement la résolution vers les éléments de boîte de réception associés.
La table des tâches
Chaque chantier se décompose en tâches. La table correspondante relie chaque tâche à un travail parent (clé étrangère logique) et expose les colonnes suivantes :
title— intitulé de la tâchestatus— état courant, défaut'todo'priority— niveau de priorité, défaut'P2'assignee_codename— codename de l'agent ou de l'automate assignéestimated_tokens/actual_tokens/actual_cost_usd— métriques de consommation IArecommended_model— modèle de langage recommandé pour cette tâcheposition— ordre d'affichage dans le travailscope— périmètre fonctionnel (enum contraint :synedre-internal,codemyshop-oss,codemyshop-enterprise,tenant-single,tenant-multi,infra,doctrine)visual_intent— description de ce qui doit être observable à l'écran après la modification (NULL = tâche non visuelle) ; exploité par le moteur de vérification visuelle automatiséevisual_url— URL de vérification visuelle (NULL = URL de staging du chantier par défaut)
Les colonnes visual_intent et visual_url ont été ajoutées en juin 2026 via une migration idempotente (ADD COLUMN IF NOT EXISTS). Elles sont reconnues par la couche Python de gestion des chantiers mais pas encore par le schéma TypeScript de l'application web — c'est une divergence volontaire et documentée : la migration a été appliquée manuellement côté base de données, hors du périmètre du détecteur de dérive de schéma, qui ne surveille qu'une sous-partie des définitions TypeScript.
Les dépendances inter-tâches sont modélisées dans une table dédiée, formant un graphe orienté.
Le journal des erreurs et victoires (cicatrices)
Le journal des apprentissages enregistre aussi bien les erreurs que les réussites. Ses colonnes principales :
agent_codenameNOT NULL — agent concernéerror_type— catégorie de l'erreurdescriptionNOT NULL — récit de l'événementroot_cause— cause racine identifiéecorrected_by— auteur de la correctionseverity— gravité :low|medium|high|criticalkind— nature de l'entrée :'failure'(défaut) ou'victory'(gravée via la compétence dédiée)resolved— smallint contraint à {0, 1, 2}tags— tableau de mots-clés (text[])importance— score de 1 à 10recall_count— nombre de fois où cette entrée a été rappeléelearnable— indique si l'entrée est exploitable pour l'apprentissage
Ce journal est exclusivement interne : il ne comporte pas de colonne de langue, son contenu n'est pas destiné à l'utilisateur final.
Les agents : une vue de compatibilité, pas une table physique
Piège de reprise. La table physique des agents est distincte de la vue que la plupart des requêtes interrogent. La vue est un shim de rétrocompatibilité issu d'une migration passée.
La table physique des agents expose les colonnes codename, nickname, role, group_name, active, diverses colonnes de configuration de poste (job_*), cognitive_frame et heritage. La vue de compatibilité en est une projection directe (SELECT … FROM <table-physique>), transparente en lecture — les requêtes existantes fonctionnent sans modification.
La même mécanique de vue-shim couvre une douzaine d'autres paires table/vue, assurant la continuité des références historiques. Au total, la base de données héberge seize vues, dont :
- Douze vues de compatibilité couvrant agents, automates, activité, battements de cœur, relations, expérience, conduites, logs et automatisation avancée
- Une vue d'événements de spawn Atlas
- Trois vues analytiques SRE :
- Vue d'agrégation quotidienne des erreurs — comptage des entrées du journal des 14 derniers jours, par tenant, jour, gravité et nature
- Vue de suivi des rollbacks — sur les runs des 14 derniers jours : total de runs, nombre de rollbacks, taux de rollback en pourcentage, nombre de verdicts bloquants, date du dernier run
- Vue de fréquence des avertissements — sur 30 jours, fréquence de chaque code d'avertissement issu des runs, nombre de verdicts bloquants associés, dernière occurrence
Avertissement. Une vue d'unification des erreurs (réunissant erreurs client-side et erreurs serveur) est référencée dans le manifeste du produit enterprise et interrogée par l'API, mais elle n'est pas instanciée dans la base principale du vaisseau-mère : la table des erreurs client-side n'y existe pas. Le décompte réel est donc seize vues, non dix-sept. Cette vue existe en revanche dans les bases propres au produit, où elle a suivi automatiquement un renommage de table en août 2026 — une vue PostgreSQL stockant un OID et non un nom, elle est immunisée contre les renommages.
À confirmer. Ces vues de compatibilité sont probablement en lecture seule. Toute opération d'écriture (
INSERT,UPDATE,DELETE) doit cibler la table physique sous-jacente et non la vue. À vérifier systématiquement avant toute modification des enregistrements agents.
Accès à la base de données en production — architecture multi-tenant
Résolution du tenant et sélection de l'adaptateur
L'accès à la base de données au sein de l'application web suit une chaîne de résolution en deux étapes. À chaque requête entrante, le système détermine d'abord à quel tenant elle appartient, puis sélectionne l'adaptateur de base de données approprié.
Étape 1 — Résolution de l'identifiant tenant : la logique de résolution consulte les sources suivantes, dans l'ordre de priorité :
- La configuration d'exécution de l'application (
runtimeConfig.clientId), définie au niveau du VPS client — chaque VPS tenant porte son propre identifiant. - Le nom d'hôte de la requête : un mot-clé est extrait du hostname, normalisé (suppression du suffixe de version
-v2, suppression des tirets), puis comparé aux clés de la carte de configuration des bases de données. - En l'absence de correspondance, l'identifiant par défaut du vaisseau-mère est utilisé.
Étape 2 — Sélection de l'adaptateur : une fois l'identifiant tenant connu, le système vérifie si l'accès PostgreSQL est autorisé pour ce tenant. Deux conditions doivent être simultanément vraies :
- Le drapeau global PostgreSQL doit être activé (voir ci-dessous).
- Le tenant doit figurer dans la liste des tenants du vaisseau-mère ou dans la liste d'activation explicite par variable d'environnement.
Si l'une ou l'autre condition échoue, la requête est rejetée : le chemin MySQL a été supprimé lors d'une phase de migration antérieure (fin avril 2026) et ne constitue plus un repli disponible.
Le drapeau global PostgreSQL — point de vigilance critique
Attention — risque d'interruption totale. Un drapeau global contrôle l'accès PostgreSQL pour l'ensemble des tenants, y compris ceux du vaisseau-mère. Si la variable d'environnement correspondante ne contient pas le caractère générique
*, aucun tenant ne peut accéder à la base de données PostgreSQL, quelle que soit sa présence dans les listes d'activation. En production, ce drapeau est positionné à*(accès universel). Vider cette variable revient à couper tout accès PostgreSQL en une seule opération.
Ce mécanisme est un héritage de la stratégie de migration progressive : lors de la phase initiale, seuls certains modules étaient activés sur PostgreSQL ; le caractère générique a été introduit au moment du basculement global. Il subsiste aujourd'hui comme interrupteur d'urgence pour un retour arrière complet.
La carte de configuration des bases de données par tenant
La carte de configuration est construite dynamiquement au démarrage de l'application à partir de variables d'environnement suivant la convention NUXT_TENANT_DB_<IDENTIFIANT>, où <IDENTIFIANT> est le codename du tenant en majuscules avec tirets convertis en underscores. La valeur de chaque variable encode, dans l'ordre : le nom de la base de données, l'hôte, le port, l'utilisateur et le mot de passe — séparés par des virgules.
Une fonction utilitaire permet d'obtenir la liste des identifiants de tenants connus, utilisée notamment pour diffuser des secrets globaux vers chaque base de données tenant concernée.
La convention « un tenant = un codename canonique, partout » s'applique à une quinzaine de points de surface dans le système (configuration, base de données, routing, etc.). La logique de résolution décrite ci-dessus en fait partie et doit toujours retourner ce codename canonique.
L'adaptateur de base de données PostgreSQL
Le runtime de base expose une interface unifiée query / get / run — identique à l'ancienne couche MySQL — mais dispatche désormais vers la bibliothèque postgres-js. Un pipeline de réécriture SQL convertit à la volée les requêtes MySQL héritées en SQL PostgreSQL valide.
Le pool de connexions
L'adaptateur instancie un singleton de connexion PostgreSQL avec les paramètres suivants :
- Hôte, port, utilisateur et base de données configurables via variables d'environnement, avec des valeurs par défaut raisonnables.
- Le mot de passe de base de données est obligatoire : l'adaptateur lève une erreur au démarrage si la variable correspondante est absente.
- Pool dimensionné à 20 connexions maximum, avec un timeout d'inactivité de 60 secondes et une durée de vie maximale de 30 minutes par connexion.
- Un schéma de base de données cible est défini comme constante et préfixe automatiquement les tables lors de la réécriture SQL.
Pipeline de réécriture SQL MySQL → PostgreSQL
Chaque requête passe par un pipeline de transformations appliquées dans l'ordre suivant :
| # | Transformation | Détail |
|---|---|---|
| 1 | Backticks → guillemets doubles | `col` → "col" |
| 2 | Qualification par schéma | Après FROM, JOIN, INTO, UPDATE, TABLE : les tables des familles catalogue et panier sont préfixées par le schéma cible. |
| 2b/2c | DATE_SUB / DATE_ADD |
Convertis en arithmétique PostgreSQL native ; gère DAY, MONTH, YEAR, HOUR, MINUTE, SECOND — aussi bien en littéral qu'avec un placeholder. |
| 2d | TIMESTAMPDIFF(unit, a, b) |
→ FLOOR(EXTRACT(EPOCH FROM (b - a)) / diviseur) pour SECOND, MINUTE, HOUR, DAY. |
| — | IFNULL(a, b) |
→ COALESCE(a, b) (PostgreSQL ne connaît pas IFNULL). |
| — | INSERT IGNORE INTO … |
→ INSERT … ON CONFLICT DO NOTHING (ignoré si ON CONFLICT est déjà présent). |
| — | Auto-quotage des alias en camelCase | AS fooBar → AS "fooBar" — préserve la casse, car PostgreSQL normalise en minuscules les identifiants non quotés. Exception : les types natifs PostgreSQL (TEXT, INTEGER, etc.) sont laissés tels quels pour ne pas briser les expressions CAST(x AS TEXT). |
| 3 | Placeholders positionnels | ? → $1, $2, … via un parseur caractère par caractère qui ignore les ? à l'intérieur de chaînes quotées. |
Important — sécurité des paramètres. La transformation des placeholders se limite à renommer les marqueurs dans le texte SQL (
?→$N). Le binding réel des valeurs est délégué à postgres-js via son API de requête paramétrée : le tableau de paramètres est transmis tel quel, sans interpolation dans la chaîne SQL. Il n'existe donc aucun risque d'injection SQL sur les paramètres.
Fonctions SQL non gérées automatiquement
Les constructions suivantes ne sont pas réécrites automatiquement par le pipeline ; tout endpoint qui en a besoin doit fournir une branche PostgreSQL dédiée :
ON DUPLICATE KEY UPDATELAST_INSERT_ID()GROUP_CONCATFIND_IN_SETDATE_FORMATCURDATE()
Interface client et émulation de insertId
L'adaptateur expose trois méthodes :
query<T>(sql, params?)— retourne un tableau de lignes typées.get<T>(sql, params?)— retourne la première ligne ounull.run(sql, params?)— retourne{ affectedRows, insertId }, émulant ainsi le comportement MySQL.
L'émulation de insertId fonctionne par injection automatique d'une clause RETURNING id_<entité> sur les requêtes INSERT qui n'en comportent pas déjà une. Cette convention repose sur la règle de nommage des clés primaires du schéma catalogue.
Deux catégories de tables sont exclues de cette injection automatique, car elles ne possèdent pas de clé primaire simple :
- Les tables à clé primaire composite (groupes de clients, associations catégorie-produit, zones de transporteur, accessoires, croisements de catégories, etc.) — listées explicitement dans la configuration de l'adaptateur.
- Toute table dont le suffixe indique une déclinaison par langue ou par boutique (
_lang,_shop), détectée par expression régulière.
Drizzle ORM — schéma-as-code pour les migrations DDL
Les deux chemins décrits précédemment — le connecteur PostgreSQL côté interface web et le connecteur Python côté outillage — font du DML : ils lisent et écrivent des lignes contre une structure de table supposée déjà en place. Aucun des deux ne crée ni n'altère de table. La structure (DDL — CREATE TABLE, colonnes, types, index, contraintes) est régie par un troisième chemin : Drizzle ORM, utilisé en mode schema-as-code. Les schémas TypeScript sont la source de vérité déclarée de la structure des tables ; le SQL CREATE/ALTER est généré (ou écrit) puis appliqué à la base de données, jamais l'inverse.
Drizzle a migré d'un dialecte MariaDB vers PostgreSQL lors d'un chantier de modernisation de la plateforme. Les dépendances actives sont drizzle-orm et drizzle-kit dans leurs versions courantes.
6.1 Fichier de configuration Drizzle
Le fichier de configuration Drizzle (à la racine du dépôt) définit les paramètres suivants :
| Clé | Valeur | Note |
|---|---|---|
dialect |
'postgresql' |
Remplace l'ancien dialecte MariaDB |
schema |
3 globs TypeScript | Périmètres core, modules cockpit, packs enterprise |
out |
Dossier des migrations SQL générées | Dans le périmètre core du dépôt |
schemaFilter |
Schéma applicatif dédié | Restreint l'introspection et le diff à ce seul schéma (ignore public et les schémas natifs) |
dbCredentials |
Hôte, port, utilisateur, mot de passe DB, nom de base | Toutes valeurs portées par variables d'environnement, jamais en clair |
strict / verbose |
true / true |
Confirmation avant push, sortie détaillée |
⚠️ Le port configuré par défaut dans
drizzle-kitest le port d'exposition TCP de la base de données, distinct du port d'accès direct au conteneur.drizzle-kitest conçu pour pointer une base exposée en TCP et ne passe pas par un accès direct au conteneur.
6.2 Commandes drizzle-kit
Aucun script npm dédié n'est exposé dans la configuration du projet : les commandes sont lancées via npx.
| Commande | Effet |
|---|---|
npx drizzle-kit generate |
Compare les schémas TS à l'état connu et écrit une nouvelle migration SQL dans le dossier out/ |
npx drizzle-kit migrate |
Applique les migrations en attente à la base cible définie dans dbCredentials |
npx drizzle-kit introspect |
Génère des fichiers TypeScript à partir d'une base existante (utile pour adopter une table legacy non encore déclarée) |
6.3 Les schémas TypeScript
Chaque fichier de schéma déclare une ou plusieurs tables via l'API pgSchema(...).table(...) de Drizzle. Les déclarations sont distribuées dans trois périmètres correspondant aux trois globs de la configuration :
| Périmètre | Contenu | Audité par le détecteur de dérive |
|---|---|---|
| Core open-source | Tables de la plateforme e-commerce (environ 85 fichiers, dont des re-exports bridge) | ✅ Oui |
| Modules cockpit vaisseau-mère | Tables agents, chantiers, travaux, invoicing, drill… (environ 47 fichiers) | ❌ Non |
| Packs enterprise | Extensions métier (environ 14 fichiers) | ❌ Non |
Piège : certains fichiers de schéma existent dans le dépôt à titre d'illustration et de référence de typage, mais ne correspondent à aucun des trois globs actifs de la configuration Drizzle. Ces fichiers ne constituent pas une entrée active du DDL. De même, les tables du cockpit (périmètres 2 et 3) ne déclenchent pas de dérive bloquante depuis le détecteur de dérive — c'est pourquoi des colonnes ajoutées récemment peuvent coexister en base et dans le code sans être couvertes par l'audit Drizzle du périmètre core.
Ce que les schémas TypeScript expriment : nom physique de colonne, type PostgreSQL, notNull, default/defaultNow, clé primaire simple ou composite, unique, index. Les types métier sont raffinés via $type<...>() (contrainte TypeScript sans répercussion en base). Exemple simplifié de déclaration :
export const appSchema = pgSchema('<nom-du-schema>')
export type Runtime = 'ps' | 'nuxt'
export type ModuleStatus = 'active' | 'disabled' | 'deprecated'
export const moduleRegistryTable = appSchema.table('<nom-de-table>', {
id: serial('id').primaryKey(),
codename: varchar('codename', { length: 128 }).notNull().unique(),
version: varchar('version', { length: 32 }).notNull(),
runtime: varchar('runtime', { length: 4 }).$type<Runtime>().notNull().default('ps'),
status: varchar('status', { length: 10 }).$type<ModuleStatus>().notNull().default('active'),
manifestJson: text('manifest_json').$type<ManifestType | null>(),
// … colonnes d'audit (hash de schéma, dates de migration, timestamps)
}, (t) => ({
idxRuntimeStatus: index('idx_runtime_status').on(t.runtime, t.status)
}))
export type Row = typeof moduleRegistryTable.$inferSelect
export type RowInsert = typeof moduleRegistryTable.$inferInsert
Points à retenir :
- Les anciens types
ENUMMariaDB ont été portés envarchar(N) + $type<Union>(): la contrainte est appliquée au niveau TypeScript, pas via un typeENUMPostgreSQL natif. - Les tables avec clé primaire composite (pattern fréquent sur les tables de traduction) utilisent
primaryKey({ columns: [t.idFoo, t.idLang] }). - Les types
$inferSelect/$inferInsertexportés sont consommables côté code applicatif. Le connecteur PostgreSQL du runtime web passe par son propre adaptateur (pas par le query-builder Drizzle) — ici Drizzle ne sert qu'au DDL et au typage statique.
6.4 Les migrations générées
Le dossier des migrations core contient à ce jour neuf migrations SQL numérotées, couvrant notamment : l'ajout d'une colonne téléphone sur la table client (inscription B2B/C), des tables de mapping d'import, d'alertes de stock, de contexte de méga-menu et de liens d'en-tête. Un fichier de journal meta/_journal.json est présent.
Fait structurant : le fichier de journal
meta/_journal.jsoncontient une liste d'entrées vide. Le tracking automatique dedrizzle-kit migraten'est donc pas la voie opérante. Les fichiers SQL du dossierout/sont en réalité écrits de façon idempotente (CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS) et appliqués manuellement par client. En pratique : le schéma TypeScript Drizzle est la source de vérité déclarative ; l'application réelle reste une opération psql manuelle par client, jamais auto-propagée.
Deuxième voie de migrations — tables du cockpit : les tables du cockpit vaisseau-mère, qui vivent dans les modules et ne sont pas auditées par le détecteur de dérive, utilisent un système de migrations SQL manuelles séparé, réparti sur deux dossiers :
- Migrations exclusives au cockpit (synedre.com, Odyssée, documentation, routage IA…) : fichiers nommés
<YYYYMMDD>_<sujet>.sql, tous idempotents. Exemples récents : ajout de colonnes d'intention visuelle sur la table des travaux, création de la table de routage IA avec seed initial, tables de revue externe et de miroir public de documentation. - Migrations appliquées directement à la base locale : tables du périmètre cockpit hors voie principale. Ce dossier accueille également des sous-dossiers par client et deux dossiers d'archivage (
applied/et_applied/) pour les migrations déjà exécutées.
L'application est manuelle dans les deux cas — ces migrations ne transitent pas par le pipeline de déploiement automatisé ni par drizzle-kit migrate.
6.5 Le détecteur de dérive DDL — garde-fou anti-dérive schéma ↔ base live
L'application manuelle des migrations sur plusieurs clients crée un risque : un ALTER appliqué sur la base centrale mais oublié sur la base d'un client partirait en production silencieusement. Des incidents passés (propagation d'ALTER, colonne absente en production) ont motivé la création d'un outil dédié : le détecteur de dérive DDL.
Fonctionnement :
- Un parseur regex lit les fichiers TypeScript de schéma du périmètre core et en extrait la carte
{table → {colonnes}}. - Un interrogateur de base requête
information_schema.columnspour chaque client cible, filtré sur le schéma applicatif et les préfixes de tables concernés. - Le diff par client distingue deux niveaux : bloquant = table ou colonne déclarée en TypeScript mais absente de la base (migration manquante) ; informatif = présent en base mais pas en TypeScript.
- Un fichier de configuration de faux positifs permet d'exclure certaines tables selon le contexte : tables cockpit-only ignorées sur les clients boutique, tables boutique exclues de la base centrale depuis la séparation des périmètres.
Codes de sortie : 0 pas de dérive bloquante ; 1 dérive bloquante (au moins un client) ; 2 erreur d'exécution.
Usage en ligne de commande :
# Audit de la base centrale uniquement (comportement par défaut, rétro-compatible)
python3 <detecteur-de-derive>
# Audit d'un client spécifique
python3 <detecteur-de-derive> --tenant <ID>
# Audit de tous les clients
python3 <detecteur-de-derive> --all
# Mode verbeux
python3 <detecteur-de-derive> --all --verbose
Intégration en gate de déploiement (fait vérifié) : le détecteur est câblé de façon bloquante dans le pipeline de déploiement. Il est exécuté avant toute construction ou poussée d'artefact. En cas de dérive détectée, le déploiement s'arrête immédiatement — empêchant de pousser du code qui attendrait des colonnes absentes de la base client. L'audit est un check pré-déploiement ; il n'existe pas de cron périodique dédié.
Pipeline de déploiement complet (ordre des étapes) :
- Vérification de dérive DDL (bloquante)
- Application automatique du drift DDL (si activée)
- Hooks background
- Génération de routes i18n pilotée par la base
- Build de l'interface web
- Seed i18n
- Source maps
- Création de l'archive de déploiement
- Push git en arrière-plan
- Upload de l'archive
- Rechargement distant (rechargement graceful du gestionnaire de processus ou du conteneur)
- Installation du cron client sur le VPS (optionnel, si déclaré dans la configuration du client)
- Attente des hooks background
- Vérification de santé (health check)
- Préchauffage du cache SSR en arrière-plan (non bloquant, lancé après le health check ; désactivable par variable d'environnement)
- Affichage du bandeau de fin
6.6 L'applicateur automatique de dérive DDL
Complément du détecteur (§6.5) : cet outil corrige la dérive au lieu de seulement la signaler.
Principes fondamentaux :
- Jamais de
DROP(ni table, ni colonne). On n'ajoute que ce qui manque. - Idempotent : rejouable sans effet si la base est déjà à jour.
- Transaction unique
BEGIN … COMMITavec arrêt sur erreur : rollback total si une instruction échoue. - Génère uniquement
ADD COLUMN IF NOT EXISTSetCREATE TABLE IF NOT EXISTS, avec types,NOT NULL/DEFAULTet contraintesCHECKdérivés des schémas TypeScript.
L'outil réutilise les définitions du détecteur de dérive (liste des clients cibles, parseur de schémas, interrogateur de base, configuration de faux positifs) pour garantir une source de vérité unique.
Pipeline interne :
- Parseur de schémas TS enrichi — construit des structures typées (types PG, nullabilité, valeur par défaut, contraintes CHECK). Correctif notable : un index Drizzle déclaré sur plusieurs lignes avec virgule finale produisait un dernier token vide, ce qui générait un
CREATE UNIQUE INDEX (..., "")rejeté par PostgreSQL avec une erreur d'identifiant de longueur nulle, provoquant un rollback automatique et l'annulation du déploiement. Le correctif filtre les tokens vides après découpage. - Calcul du drift — différence TypeScript ↔ base live : tables absentes →
CREATE TABLE; colonnes absentes →ADD COLUMN. - Génération du SQL — bloc SQL idempotent prêt à l'application.
- Application — exécutée en transaction sur le client cible, uniquement si l'option
--applyest passée.
Modes d'usage :
| Commande | Effet |
|---|---|
python3 <applicateur-derive> --tenant <ID> --dry-run |
Affiche le SQL sans l'exécuter (mode par défaut) |
python3 <applicateur-derive> --tenant <ID> --apply |
Applique en transaction |
python3 <applicateur-derive> --all --dry-run |
Inspecte tous les clients |
Codes de sortie : 0 pas de drift (ou appliqué avec succès) ; 1 drift détecté en --dry-run ou erreur lors de l'application ; 2 erreur de parsing ou de connexion.
L'applicateur est appelé par la fonction d'application automatique du pipeline de déploiement avec l'option --apply. Cette fonction est active dans le chemin de déploiement unifié que tous les clients empruntent depuis la consolidation des scripts de déploiement per-client (désormais supprimés) — le gap historique est donc fermé.
Comportement par défaut de l'application automatique : activée (DRIFT_AUTO_APPLY=1) depuis un correctif de juin 2026 (l'ancienne valeur par défaut était 0). L'opt-in réel est la présence d'une clé drift: dans la configuration YAML du client ; si elle est absente, le bloc d'application est sauté et l'applicateur n'est jamais appelé.
6.7 Qui fait foi pour l'évolution structurelle
Schémas TypeScript (périmètre core + modules)
│ (1) On édite le fichier TypeScript
▼
Migration SQL idempotente (dossier out/ ou dossiers de migrations manuelles)
│ (2a) Application psql manuelle sur chaque client impacté
│ (2b) Applicateur automatique de dérive via le pipeline de déploiement
▼
Base PostgreSQL live (base centrale + clients)
▲
└─ (3) Détecteur de dérive vérifie TypeScript == live,
bloque le déploiement si écart bloquant détecté
Règle de reprise : pour faire évoluer une table, on édite d'abord le schéma TypeScript, on génère ou écrit la migration idempotente, on l'applique par client (voie manuelle ou automatique), puis on relance l'audit — le déploiement le rejoue de toute façon. Il ne faut jamais modifier la base live sans répercuter le changement dans le schéma TypeScript : l'audit le signalerait comme bloquant au prochain déploiement.
Inversement, les classes d'entités Python du côté outillage ne créent jamais de colonne : leur liste de champs est une whitelist de colonnes supposées déjà présentes en base, pas une déclaration de structure.
Accès agentique (Python) — le pattern Entity
Le code Python d'orchestration repose sur une classe de base dont héritent une quarantaine de classes spécialisées, chacune représentant une entité métier du cockpit : chantier, travail, tâche, agent, doctrine, facturation, etc.
Transport vers la base de données
Contrairement au runtime front-end qui utilise un pool de connexions TCP, la couche Python accède à la base de données via des appels système au moteur PostgreSQL conteneurisé. Trois helpers distincts couvrent les cas d'usage :
| Helper | Usage | Note |
|---|---|---|
| Écriture par fichier temporaire | Toutes les opérations d'écriture | Le SQL est écrit dans un fichier temporaire à nom unique (pid + uuid pour éviter les collisions multi-process), copié dans le conteneur, puis exécuté avec arrêt immédiat sur erreur — pas d'injection par stdin. |
| Lecture inline tab-séparée | Lectures simples | SQL passé directement au moteur ; à éviter sur les colonnes TEXT contenant des sauts de ligne. |
| Lecture inline CSV | Lectures avec sauts de ligne potentiels | Mode CSV (RFC 4180), les valeurs NULL sont normalisées en chaîne vide. |
Les trois helpers préfixent automatiquement chaque requête avec la déclaration du schéma de travail. Les paramètres de connexion (conteneur cible, nom de base, utilisateur, mot de passe DB, schéma) sont lus depuis les variables d'environnement au démarrage.
La classe de base Entity
Le CRUD générique est paramétré par trois attributs de classe : le nom de la table cible, la clé primaire, et la liste blanche des colonnes acceptées en insertion ou mise à jour.
- Création : validation métier, forçage de l'identifiant client canonique si la colonne est whitelistée, filtrage sur la liste blanche, puis
INSERT … RETURNING <pk>avec horodatage automatique. - Mise à jour : même filtrage, mise à jour de
date_updautomatique. - Lecture / existence / suppression : méthodes
find,find_one,exists,delete. - Échappement : un helper interne normalise les booléens en
TRUE/FALSESQL et sérialise les dicts et listes en JSON.
Les sous-classes surchargent validate(data, mode) pour leurs règles métier spécifiques : les erreurs bloquantes lèvent une ValidationError ; les avertissements non bloquants sont collectés et retournés dans la réponse.
Création atomique d'un chantier avec squelette
La méthode de création de chantier interdit les chantiers orphelins en créant en une seule transaction le chantier, son premier travail, et au moins une tâche. La signature simplifiée :
create_with_skeleton(
codename, title, *,
first_travail: dict,
first_taches: list[dict] | None,
first_tache: dict | None,
client_id=None, priority="P2", scope=None,
current_focus=None, notes=None,
) -> {id_chantier, id_travail, id_tache, id_taches, codenames, warnings}
Les champs suivants sont transmis en transparence à chaque tâche créée : priority, description, estimated_tokens, estimated_h, position, recommended_model, scope, visual_intent, visual_url.
Validations bloquantes (lèvent ValidationError) :
- Le
codenamedoit respecter le format kebab-case (4 à 64 caractères) et être unique. - La
prioritydoit appartenir à{P0, P1, P2, P3}. - Chaque tâche doit avoir un
titleet unassignee_codenamenon vides, et l'agent assigné doit exister dans le registre des agents. - Recrutement multi-agents : si le
scopecommence partenant, au moins deux agents distincts doivent être assignés — mécanisme issu de la doctrine de supervision croisée.
Avertissement non bloquant : estimated_tokens manquant sur une tâche.
La transaction enchaîne les insertions (chantier, puis travail retrouvé par codename, puis chaque tâche retrouvée par id_travail) avec arrêt immédiat sur erreur garantissant le rollback complet. Le modèle IA recommandé par tâche est auto-calculé si absent.
Entité Tâche — estimation, modèle, compétences et outils
- Création : si
estimated_tokensest absent, un estimateur automatique est appelé ; le champscopeest validé contre la liste des valeurs autorisées ; un avertissement non bloquant est émis si l'agent assigné n'appartient pas à l'équipe de production du chantier. - Recommandation de modèle : heuristique à trois niveaux — modèle le plus puissant si la priorité est P0, si l'agent a ≥ 2 itérations en échec, ou si la tâche dépasse 8 000 tokens estimés ; modèle intermédiaire au-dessus de 1 500 tokens ; modèle léger sinon.
- Rattachement de compétence : recherche de la compétence par nom (clé naturelle), insertion dans la table de liaison avec
ON CONFLICT … DO NOTHING. Si la compétence est inconnue, une proposition est enregistrée avec le statutpendinget la méthode retourneFalse. - Rattachement d'outil : même mécanique par slug, avec proposition en cas d'outil inconnu.
Cascades de statut
Les mises à jour de statut sur les tâches et les travaux déclenchent automatiquement des cascades vers les niveaux supérieurs :
Tâche → done/cancelled (toutes les tâches du travail terminales, ≥1 done)
└─ Cascade tâche → travail
├─ Équipe QA recrutée ? → applique le verdict de validation
└─ Sinon → travail = 'done'
Travail → done/cancelled (tous les travaux du chantier terminaux, ≥1 done)
└─ Cascade travail → chantier
├─ Garde-fou discovery-only : tous done en phase 'discovery'
│ → auto-explosion par LLM (crée les travaux d'implémentation)
├─ Sinon → chantier = 'test' (préprod, attente review)
└─ Avertissement non bloquant si plan de test préprod ou commande de livraison manquants
Un mécanisme de résolution complémentaire permet à un travail correctif lié à un travail parent (via un champ de référence) de fermer automatiquement ce parent lorsqu'il passe à done : les tâches non démarrées du parent sont annulées et la décision est enregistrée dans le journal d'audit.
Conventions d'internationalisation et polymorphisme
Tables de traduction (_lang)
Tout texte visible par un visiteur doit résider dans une table sœur suffixée _lang, jamais dans la table parente.
- Le suffixe est exactement
_lang(pas_translation,_i18n, ni_locale). - La clé primaire est composite
(id_<entité>, id_lang), sans auto-incrément. En contexte multi-boutique, la clé est étendue à(id_<entité>, id_lang, id_shop)— jamais de table_shop_langséparée. - Séparation stricte : la table parente porte les clés étrangères, les flags, les dates et les énumérations ; la table
_langporte les champs texte (title,description,meta_*) et tout contenu visible.
Cette convention a un impact direct sur l'adaptateur de base de données : les tables _lang sont exclues de l'heuristique de récupération d'identifiant généré, car elles ne possèdent pas de clé primaire simple. Côté front, les chaînes passent par une fonction de traduction qui lit la table des traductions ; les chaînes codées en dur sont interdites.
Polymorphisme par parent_type / parent_id
Lorsqu'une fonctionnalité s'applique à plusieurs types d'entités parentes, on utilise une seule table polymorphique — jamais une table par type de parent :
-- Exemple : table de FAQ polymorphique
id_faq integer -- clé primaire
parent_type varchar -- 'cms' | 'category' | 'product'
parent_id integer
position / active / date_add / date_upd
-- + table _lang (id_faq, id_lang, question, answer)
Exception : l'extension 1:1 d'une entité native de la plateforme e-commerce suit le pattern <préfixe>_<entité>_extra, dont la clé primaire est aussi clé étrangère vers l'entité d'origine — pas de polymorphisme dans ce cas. Les tables de liaison N-N pures suivent une convention de nommage alphabétique sans suffixe _asso ou _link, et ne portent pas de table _lang.
Pas de JSON métier en colonne
Les colonnes JSON de contenu métier sont interdites (payload_json, content_i18n, labels_json…). La seule tolérance concerne les payloads techniques éphémères (webhooks, logs, état de session), documentés par un commentaire de colonne. Dans le cockpit, les champs JSON des travaux et des tâches servent exclusivement à l'audit-trail technique en mode append-only — c'est la tolérance, pas la règle générale. Un outil de vérification de schéma (cron quotidien à 04:15 UTC) signale les violations en priorité P0.
Pièges de reprise — récapitulatif
- La vue des agents n'est pas la table physique. La lecture se fait via la vue exposée publiquement ; les écritures doivent cibler la table physique sous-jacente. Ce principe s'applique à une douzaine de paires vue/table similaires dans le schéma (agents, activité, heartbeat, relations, XP, historique XP, automates, liaisons automate-agent, conduites, logs d'automate, logs de smart-automation, règles de smart-automation).
- Conteneur ≠ base ≠ schéma. Le conteneur PostgreSQL, la base de données et le schéma de travail sont trois identifiants distincts — les confondre produit des erreurs de connexion silencieuses.
- L'adaptateur front-end rejette tout tenant non migré. Le chemin de compatibilité MySQL a été supprimé ; un tenant doit être déclaré dans la liste d'activation PostgreSQL pour être servi.
- L'énumération
scopeen base fait foi. La contrainteCHECKPostgreSQL est plus large que la doctrine écrite ; en cas de doute, interroger la contrainte directement. - Résidu de renommage sur les séquences de tâches. La table des tâches a conservé des séquences et des noms de contraintes issus de son ancien nom — ne pas s'y fier pour inférer des conventions.
- Le SQL MySQL legacy doit être converti manuellement. Le convertisseur automatique ne couvre pas toutes les fonctions MySQL (
GROUP_CONCAT,DATE_FORMAT,ON DUPLICATE KEY UPDATE…). Les fonctions non couvertes échouent silencieusement et exigent une branche de remplacement écrite à la main.