[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f4in77FidrPxQ44Ko-QkDW-SMt7DBXlVazTBDFYM4QBE":3,"$fk1oB-e5O_BgwF2c-QwDgsx2Y7SMPhf77z9g2mrpoBuM":23},{"chapter":4,"prev":13,"next":18},{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10,"contentHtml":11,"contentHtmlEn":12},"data-layer","02","La couche données","The Data Layer","Décrit l'architecture données de Synedre OS : une base PostgreSQL unique (un composant interne\u002Fun composant interne), les trois chemins d'accès (Nuxt\u002FNitro, Python agentique, Drizzle ORM DDL), les familles de tables par préfixe et les conventions de nommage associées.","Describes the Synedre OS data architecture: a single PostgreSQL database (un composant interne\u002Fun composant interne), the three access paths (Nuxt\u002FNitro, agentic Python, Drizzle ORM DDL), the table families by prefix, and the associated naming conventions.","\u003Ch2 id=\"data-layer-overview\">La couche données de Synedre OS\u003C\u002Fh2>\n\n\u003Cp>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 les requêtes SQL côté interface web, des classes Python côté outillage agentique, un système de schéma-en-code pour les migrations, et les conventions de nommage qui font tenir l'ensemble. Elle est destinée aux ingénieurs qui reprennent le code.\u003C\u002Fp>\n\n\u003Ch2 id=\"db-unique\">1. La base de données centrale\u003C\u002Fh2>\n\n\u003Cp>Toute la donnée du vaisseau-mère vit dans \u003Cstrong>un seul schéma PostgreSQL\u003C\u002Fstrong>, hébergé dans un container dédié sur le VPS du vaisseau-mère. La base et le schéma portent des noms distincts — point qui surprend à la reprise : le container, la base et le schéma ont trois identifiants différents, et les tables du harness vivent dans le schéma spécifique, pas dans le schéma \u003Ccode>public\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>Les credentials d'accès (utilisateur DB, mot de passe) sont portés par les fichiers d'environnement et ne sont jamais écrits en clair dans le code ; l'application lève une erreur explicite s'ils sont absents.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Frontière harness \u002F multi-tenant.\u003C\u002Fstrong> Ce chapitre documente exclusivement le schéma central du \u003Cstrong>harness Synedre OS\u003C\u002Fstrong> (cockpit agentique privé, mono-base). Les bases de données des tenants CodeMyShop utilisent le même adaptateur de connexion (voir §4–5) mais leurs données ne croisent jamais le schéma du harness : chaque tenant possède sa propre base, son propre schéma, ses propres tables. Le §4 couvre l'adaptateur parce qu'il sert aussi au cockpit — mais toute référence aux tenants y est contextuelle, non constitutive du harness.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Trois chemins d'accès\u003C\u002Fh3>\n\n\u003Cp>Deux chemins coexistent à l'exécution (lecture\u002Fécriture de lignes — DML) ; un troisième opère au niveau structurel (évolution du schéma — DDL) :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>                   ┌──────────────────────────────────────┐\n                   │         PostgreSQL — base centrale    │\n                   │         schéma du harness Synedre OS  │\n                   └───────────────┬──────────────────────┘\n                                   │\n   ┌──────────────────┬────────────┴──────────────┬─────────────────────┐\n   │  DML — runtime   │  DML — outillage          │  DDL — structure    │\n   │ (A) Interface    │ (B) Outillage Python       │ (C) Schéma-en-code  │\n   │     web (Nuxt)   │     agentique              │     (Drizzle ORM)   │\n   │  pool TCP        │  exécution via container   │  migrations SQL     │\n   │  postgres-js     │  (fichier SQL temporaire   │  générées et        │\n   │                  │   ou requête inline)        │  vérifiées (§6)     │\n   └──────────────────┴────────────────────────────┴─────────────────────┘\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Chemin A\u003C\u002Fstrong> — l'interface web (Nuxt\u002FNitro) accède à la base via un pool de connexions TCP (\u003Cem>postgres-js\u003C\u002Fem>). Un utilitaire central expose la connexion ; un adaptateur spécifique gère la compatibilité avec les requêtes héritées (voir §4–5).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Chemin B\u003C\u002Fstrong> — l'outillage Python agentique écrit des requêtes via un mécanisme d'exécution en container : les écritures passent par un fichier SQL temporaire copié puis exécuté ; les lectures utilisent une requête inline. Aucun chemin n'emploie de pipe stdin.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Chemin C\u003C\u002Fstrong> — le DDL est géré en \u003Cem>schéma-en-code\u003C\u002Fem> (Drizzle ORM) : c'est lui qui fait foi pour l'évolution structurelle des tables. Il ne manipule pas de lignes ; il génère et applique des migrations SQL (détaillé en §6).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Instantané des familles de tables (31 mai 2026)\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Famille\u003C\u002Fth>\n      \u003Cth>Volume (tables de base)\u003C\u002Fth>\n      \u003Cth>Périmètre\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Tables legacy privées CodeMyShop \u002F mothership\u003C\u002Ftd>\n      \u003Ctd>159\u003C\u002Ftd>\n      \u003Ctd>Dette à migrer — cockpit, agents, cicatrices…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables cockpit Synedre OS\u003C\u002Ftd>\n      \u003Ctd>69\u003C\u002Ftd>\n      \u003Ctd>Agents, chantiers, runs, négociation…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables PrestaShop natives\u003C\u002Ftd>\n      \u003Ctd>18\u003C\u002Ftd>\n      \u003Ctd>Dette historique — produits, catégories, traductions…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables PaaS public (tenants)\u003C\u002Ftd>\n      \u003Ctd>≈ 1 dans ce schéma\u003C\u002Ftd>\n      \u003Ctd>Quasi absent du schéma harness ; vit surtout dans les DB tenants\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>S'y ajoutent \u003Cstrong>16 vues\u003C\u002Fstrong> dans le schéma central (voir §3.4 — point critique : la vue des agents est une projection de la table cockpit correspondante).\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>Le décompte exact évolue à chaque chantier ; les chiffres ci-dessus sont l'instantané du 31 mai 2026. La requête de vérité canonique est documentée en §7.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"familles-tables\">2. Les familles de tables et leurs conventions\u003C\u002Fh2>\n\n\u003Cp>Le préfixe d'une table encode son périmètre et son régime de propriété. Quatre familles coexistent dans l'écosystème :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Famille\u003C\u002Fth>\n      \u003Cth>Périmètre\u003C\u002Fth>\n      \u003Cth>Statut\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Tables PaaS public OSS\u003C\u002Ftd>\n      \u003Ctd>Core communautaire — FAQ, blocs d'accueil, etc.\u003C\u002Ftd>\n      \u003Ctd>Cible (dans les DB tenants)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables cockpit Synedre OS\u003C\u002Ftd>\n      \u003Ctd>Agents, chantiers, runs, négociation, tâches atomiques\u003C\u002Ftd>\n      \u003Ctd>Cible\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables legacy privées CodeMyShop\u003C\u002Ftd>\n      \u003Ctd>Ancien périmètre mothership et cockpit\u003C\u002Ftd>\n      \u003Ctd>Dette à migrer\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tables PrestaShop natives\u003C\u002Ftd>\n      \u003Ctd>Produits, catégories, traductions…\u003C\u002Ftd>\n      \u003Ctd>Dette historique\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Conventions transverses\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Nom singulier\u003C\u002Fstrong> — une table se nomme au singulier (aligné sur la convention PrestaShop natif).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Une table = une entité parente\u003C\u002Fstrong> — pas de table fourre-tout.\u003C\u002Fli>\n  \u003Cli>Les fichiers source sont nommés en \u003Cstrong>kebab-case\u003C\u002Fstrong> ; les composants Vue en \u003Cstrong>PascalCase\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Historique de migration.\u003C\u002Fstrong> La famille de tables PaaS public résulte d'un rename unique effectué en v0.2.0, depuis l'ancien préfixe legacy (à l'époque où CodeMyShop était une extension PrestaShop). Dans le schéma du harness, la majorité des tables sont restées sous l'ancien préfixe — la dette n'a pas encore été soldée — tandis que le nouveau préfixe PaaS est appliqué dans les bases tenants OSS.\u003C\u002Fp>\n\n\u003Ch3 id=\"cohabitation-chantier\">2.1 Cohabitation des familles sur une même entité logique\u003C\u002Fh3>\n\n\u003Cp>Point qui surprend à la reprise : une seule entité logique — le \u003Cstrong>chantier\u003C\u002Fstrong> — est répartie sur les deux familles legacy et cockpit.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Rôle de la table\u003C\u002Fth>\n      \u003Cth>Famille\u003C\u002Fth>\n      \u003Cth>Granularité\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Le chantier lui-même\u003C\u002Ftd>\n      \u003Ctd>Legacy privée\u003C\u002Ftd>\n      \u003Ctd>1 ligne par chantier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Les travaux granulaires\u003C\u002Ftd>\n      \u003Ctd>Legacy privée\u003C\u002Ftd>\n      \u003Ctd>N lignes par chantier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Les tâches atomiques\u003C\u002Ftd>\n      \u003Ctd>Cockpit Synedre OS\u003C\u002Ftd>\n      \u003Ctd>N lignes par travail\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Satellites cockpit (équipe, verrou multi-session, QA…)\u003C\u002Ftd>\n      \u003Ctd>Cockpit Synedre OS\u003C\u002Ftd>\n      \u003Ctd>N lignes par chantier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Artefact d'historique.\u003C\u002Fstrong> La table des tâches atomiques a été renommée lors de la migration vers la famille cockpit, mais sa séquence de clé primaire et la contrainte PRIMARY KEY conservent l'ancien nom de famille. Le rename a touché le nom de table, pas tous les objets dépendants — à corriger lors du prochain chantier de normalisation DDL.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"modele-donnees-tables-cles\">Modèle de données des tables clés\u003C\u002Fh2>\n\n\u003Ch3>La table des chantiers\u003C\u002Fh3>\n\n\u003Cp>La table centrale des chantiers repose sur une clé primaire sérielle. Ses colonnes notables sont les suivantes :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>codename\u003C\u002Fstrong> — identifiant kebab-case unique du chantier (varchar 64, obligatoire)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>title\u003C\u002Fstrong> — intitulé lisible (varchar 255, obligatoire)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>client_id\u003C\u002Fstrong> — référence optionnelle vers un client ; \u003Ccode>NULL\u003C\u002Fcode> indique un chantier interne\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>status\u003C\u002Fstrong> — état courant, défaut \u003Ccode>'planning'\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>priority\u003C\u002Fstrong> — niveau de priorité, défaut \u003Ccode>'P2'\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>current_focus\u003C\u002Fstrong>, \u003Cstrong>deadline\u003C\u002Fstrong>, \u003Cstrong>notes\u003C\u002Fstrong>, \u003Cstrong>mission_letter\u003C\u002Fstrong>, \u003Cstrong>preprod_test_plan\u003C\u002Fstrong> — champs texte de pilotage\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>external_contacts\u003C\u002Fstrong> — contacts tiers rattachés au chantier\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>ship_command\u003C\u002Fstrong> — commande de livraison associée (varchar 255)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>scope\u003C\u002Fstrong> — périmètre contraint par un enum : \u003Ccode>synedre\u003C\u002Fcode>, \u003Ccode>codemyshop-oss\u003C\u002Fcode>, \u003Ccode>codemyshop-enterprise\u003C\u002Fcode>, \u003Ccode>tenant\u003C\u002Fcode>, \u003Ccode>business\u003C\u002Fcode>, \u003Ccode>juridique\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>, \u003Ccode>conseil\u003C\u002Fcode> (ou \u003Ccode>NULL\u003C\u002Fcode>). Cet enum en base est plus large que celui documenté dans le fichier de référence racine, qui ne recense pas les valeurs \u003Ccode>juridique\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode> et \u003Ccode>conseil\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>auto_explode\u003C\u002Fstrong> — booléen, défaut \u003Ccode>true\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>mode_auto\u003C\u002Fstrong> — booléen, défaut \u003Ccode>false\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>max_cost_eur\u003C\u002Fstrong> — plafond budgétaire en euros\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>archived_at\u003C\u002Fstrong> \u002F \u003Cstrong>archived_by\u003C\u002Fstrong> — horodatage et auteur de l'archivage\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>date_add\u003C\u002Fstrong> \u002F \u003Cstrong>date_upd\u003C\u002Fstrong> — horodatages de création et mise à jour (\u003Ccode>timestamptz\u003C\u002Fcode>, défaut \u003Ccode>now()\u003C\u002Fcode>)\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Un trigger actif sur cette table se déclenche après toute mise à jour du champ \u003Ccode>status\u003C\u002Fcode> et propage la résolution vers le journal des e-mails entrants liés au chantier. Ce mécanisme de cascade garantit la cohérence entre l'état du chantier et les éléments de sa boîte de réception associée.\u003C\u002Fp>\n\n\u003Ch3>La table des tâches\u003C\u002Fh3>\n\n\u003Cp>Chaque chantier se décompose en tâches. La table des tâches est reliée au chantier via une clé étrangère logique vers la table des travaux. Colonnes clés :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>title\u003C\u002Fstrong>, \u003Cstrong>status\u003C\u002Fstrong> (défaut \u003Ccode>'todo'\u003C\u002Fcode>), \u003Cstrong>priority\u003C\u002Fstrong> (défaut \u003Ccode>'P2'\u003C\u002Fcode>)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>assignee_codename\u003C\u002Fstrong> — codename de l'agent assigné (varchar 64)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>estimated_tokens\u003C\u002Fstrong> \u002F \u003Cstrong>actual_tokens\u003C\u002Fstrong> \u002F \u003Cstrong>actual_cost_usd\u003C\u002Fstrong> — métriques de consommation IA\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>recommended_model\u003C\u002Fstrong> — modèle IA conseillé pour cette tâche (varchar 32)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>position\u003C\u002Fstrong> — ordre d'affichage\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>scope\u003C\u002Fstrong> — périmètre contraint : \u003Ccode>synedre-internal\u003C\u002Fcode>, \u003Ccode>codemyshop-oss\u003C\u002Fcode>, \u003Ccode>codemyshop-enterprise\u003C\u002Fcode>, \u003Ccode>tenant-single\u003C\u002Fcode>, \u003Ccode>tenant-multi\u003C\u002Fcode>, \u003Ccode>infra\u003C\u002Fcode>, \u003Ccode>doctrine\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>visual_intent\u003C\u002Fstrong> (\u003Ccode>text\u003C\u002Fcode>, nullable) — description de ce qui doit être visible à l'écran après le changement ; \u003Ccode>NULL\u003C\u002Fcode> indique une tâche non-visuelle. Ce champ alimente le moteur de vérification visuelle automatisée.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>visual_url\u003C\u002Fstrong> (\u003Ccode>text\u003C\u002Fcode>, nullable) — URL de vérification ; \u003Ccode>NULL\u003C\u002Fcode> renvoie vers le staging du chantier\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Les colonnes \u003Ccode>visual_intent\u003C\u002Fcode> et \u003Ccode>visual_url\u003C\u002Fcode> ont été ajoutées via une migration idempotente (\u003Ccode>ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). Elles sont reconnues par la couche Python de gestion des entités mais \u003Cstrong>pas encore\u003C\u002Fstrong> reflétées dans le schéma TypeScript côté application web — cas de divergence volontaire entre la base de données live et le schéma applicatif, hors périmètre de l'outil d'audit de dérive automatique qui ne lit que le répertoire de schémas principal.\u003C\u002Fp>\n\n\u003Cp>La table des tâches est également référencée par une table de dépendances inter-tâches, permettant de modéliser un graphe d'ordonnancement.\u003C\u002Fp>\n\n\u003Ch3>Le journal des apprentissages (cicatrices et victoires)\u003C\u002Fh3>\n\n\u003Cp>Ce journal enregistre les erreurs, leçons tirées et victoires de l'équipe d'agents. Chaque entrée porte :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>agent_codename\u003C\u002Fstrong> — codename de l'agent concerné (obligatoire)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>error_type\u003C\u002Fstrong>, \u003Cstrong>description\u003C\u002Fstrong> (obligatoire), \u003Cstrong>root_cause\u003C\u002Fstrong>, \u003Cstrong>corrected_by\u003C\u002Fstrong>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>severity\u003C\u002Fstrong> — niveau de gravité : \u003Ccode>low\u003C\u002Fcode>, \u003Ccode>medium\u003C\u002Fcode>, \u003Ccode>high\u003C\u002Fcode>, \u003Ccode>critical\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>kind\u003C\u002Fstrong> — nature de l'entrée : \u003Ccode>'failure'\u003C\u002Fcode> (défaut) ou \u003Ccode>'victory'\u003C\u002Fcode> (créée via la compétence dédiée)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>resolved\u003C\u002Fstrong> — état de résolution (valeurs entières : 0, 1 ou 2)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>tags\u003C\u002Fstrong> — tableau de mots-clés (\u003Ccode>text[]\u003C\u002Fcode>)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>importance\u003C\u002Fstrong> — score de 1 à 10\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>recall_count\u003C\u002Fstrong>, \u003Cstrong>learnable\u003C\u002Fstrong> — indicateurs de réutilisabilité pédagogique\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Ce journal est à usage interne : il ne comporte pas de colonne de langue et n'est pas exposé directement aux utilisateurs finaux.\u003C\u002Fp>\n\n\u003Ch3>Les agents : une vue de compatibilité, pas une table physique\u003C\u002Fh3>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Piège majeur lors d'une reprise.\u003C\u002Fstrong> La table physique des agents est distincte de la vue exposée historiquement. La vue est un \u003Cem>shim\u003C\u002Fem> de rétrocompatibilité issu d'une migration de consolidation antérieure.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>La table physique des agents contient les colonnes \u003Ccode>codename\u003C\u002Fcode>, \u003Ccode>nickname\u003C\u002Fcode>, \u003Ccode>role\u003C\u002Fcode>, \u003Ccode>group_name\u003C\u002Fcode>, \u003Ccode>active\u003C\u002Fcode>, des champs de configuration de poste (\u003Ccode>job_*\u003C\u002Fcode>), \u003Ccode>cognitive_frame\u003C\u002Fcode> et \u003Ccode>heritage\u003C\u002Fcode>. La vue exposée sous l'ancien nom est une simple projection \u003Ccode>SELECT … FROM\u003C\u002Fcode> de cette table physique. Toute lecture via l'ancien nom fonctionne de manière transparente ; en revanche, \u003Cstrong>les écritures doivent cibler la table physique\u003C\u002Fstrong> — les vues de ce type sont probablement en lecture seule. À vérifier avant tout \u003Ccode>UPDATE\u003C\u002Fcode> ou \u003Ccode>INSERT\u003C\u002Fcode> via l'ancien nom.\u003C\u002Fp>\n\n\u003Cp>Le même mécanisme de vue-shim couvre une douzaine de paires similaires : les automates, les activités d'agents, les heartbeats, les relations entre agents, les XP et leur historique, les automates et leurs conduites, leurs logs, ainsi que les tables de pilotage intelligent. Au total, seize vues coexistent dans le schéma de base de données, réparties comme suit :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Douze vues-shim\u003C\u002Fstrong> — redirection transparente de l'ancien espace de noms vers le nouveau\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Une vue d'événements\u003C\u002Fstrong> — liée au mécanisme de spawn Atlas\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Trois vues analytiques SRE\u003C\u002Fstrong> :\n    \u003Cul>\n      \u003Cli>Agrégation quotidienne du journal des apprentissages sur 14 jours, ventilée par niveau de gravité et nature d'entrée\u003C\u002Fli>\n      \u003Cli>Synthèse des runs de revue sur 14 jours : nombre total, nombre de rollbacks, taux de rollback, nombre de verdicts bloquants, dernier run\u003C\u002Fli>\n      \u003Cli>Fréquence des avertissements sur 30 jours : dénombrement par code d'avertissement, nombre de verdicts bloquants associés, dernière occurrence\u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Vue de suivi d'erreurs non instanciée.\u003C\u002Fstrong> Une vue d'unification des erreurs est référencée dans le manifeste du module de suivi d'erreurs et appelée par le code API, mais elle \u003Cstrong>n'existe pas en base live\u003C\u002Fstrong> : l'une de ses deux sources (les erreurs PostHog) n'est pas instanciée dans le schéma courant. Seule la source des erreurs serveur est présente. Une migration antérieure n'a donc pas pu recréer cette vue. Le décompte réel est bien \u003Cstrong>16 vues\u003C\u002Fstrong>, et non 17.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Chr>\n\n\u003Ch2 id=\"acces-runtime-multi-tenant\">Accès à la base de données en runtime — multi-tenant\u003C\u002Fh2>\n\n\u003Ch3>Résolution du client et sélection de l'adaptateur\u003C\u002Fh3>\n\n\u003Cp>Le module d'accès à la base de données expose une fonction principale qui, à partir d'une requête HTTP entrante, identifie le tenant concerné puis renvoie un adaptateur de connexion prêt à l'emploi.\u003C\u002Fp>\n\n\u003Cp>La résolution du tenant suit une cascade de trois règles, dans l'ordre de priorité :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>Un identifiant de client défini explicitement dans la configuration runtime du processus (chaque VPS client peut le définir statiquement).\u003C\u002Fli>\n  \u003Cli>Une correspondance entre le hostname de la requête et une table de mapping tenant→base de données, permettant de servir plusieurs tenants depuis une seule instance applicative.\u003C\u002Fli>\n  \u003Cli>Un repli sur l'identifiant interne du vaisseau-mère (\u003Ccode>ac-hub\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Une fois le tenant identifié, une seconde fonction détermine si la connexion PostgreSQL est activée pour ce tenant. Deux listes blanches coexistent : les tenants internes au vaisseau-mère (liste figée dans le code) et les tenants PostgreSQL activés dynamiquement via une variable d'environnement. Si le tenant n'appartient à aucune des deux listes, la fonction lève une exception — le chemin MySQL a été définitivement supprimé lors d'une phase de migration antérieure.\u003C\u002Fp>\n\n\u003Ch3>Verrou global PostgreSQL\u003C\u002Fh3>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Point de vigilance critique.\u003C\u002Fstrong> Un verrou global, contrôlé par la variable d'environnement \u003Ccode>PG_ENABLED_DOMAINS\u003C\u002Fcode>, conditionne l'accès PostgreSQL pour \u003Cem>tous\u003C\u002Fem> les tenants sans exception. Si cette variable ne contient pas le wildcard \u003Ccode>*\u003C\u002Fcode>, même un tenant figurant dans la liste blanche interne sera rejeté. En production, le basculement global est en place (\u003Ccode>*\u003C\u002Fcode>) et le verrou est ouvert. Mais vider cette variable constitue un rollback complet vers MariaDB pour l'ensemble du système — opération à manier avec la plus grande prudence.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>Ce mécanisme est un héritage de la stratégie de migration progressive : dans les premières phases, seuls certains modules étaient activés sur PostgreSQL (\u003Ccode>PG_ENABLED_DOMAINS=cms,inventory\u003C\u002Fcode>) ; le basculement global (\u003Ccode>=*\u003C\u002Fcode>) n'est intervenu qu'à la phase de coupure définitive.\u003C\u002Fp>\n\n\u003Ch3>Table de mapping tenant → base de données\u003C\u002Fh3>\n\n\u003Cp>La table de mapping est construite dynamiquement à partir de variables d'environnement suivant la convention \u003Ccode>NUXT_TENANT_DB_&lt;CODENAME_EN_MAJUSCULES&gt;\u003C\u002Fcode> (les tirets du codename étant remplacés par des underscores). La valeur de chaque variable encode, dans l'ordre : le nom de la base, l'hôte, le port, l'utilisateur et le mot de passe — séparés par des virgules. Une fonction utilitaire expose la liste des codenames tenant connus, utilisée notamment pour propager des secrets globaux vers chaque base tenant.\u003C\u002Fp>\n\n\u003Cp>La convention « un tenant = un codename canonique » s'applique à une quinzaine de surfaces du système (configuration, routing, base de données, logs…). La fonction de résolution du client en est l'une des surfaces clés.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"adaptateur-postgres\">L'adaptateur PostgreSQL\u003C\u002Fh2>\n\n\u003Cp>L'adaptateur PostgreSQL expose une interface \u003Ccode>query\u003C\u002Fcode> \u002F \u003Ccode>get\u003C\u002Fcode> \u002F \u003Ccode>run\u003C\u002Fcode> identique à l'ancienne interface MySQL, permettant une substitution transparente. Il s'appuie sur la bibliothèque \u003Cstrong>postgres-js\u003C\u002Fstrong> et assure à la volée la conversion du SQL MySQL hérité.\u003C\u002Fp>\n\n\u003Ch3>Le pool de connexions\u003C\u002Fh3>\n\n\u003Cp>Un singleton de connexion est instancié avec les paramètres suivants (lus depuis les variables d'environnement) :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Hôte, port, utilisateur et nom de base — avec des valeurs par défaut internes si les variables sont absentes\u003C\u002Fli>\n  \u003Cli>Mot de passe de la base — \u003Cstrong>obligatoire\u003C\u002Fstrong> : l'adaptateur lève une exception immédiate si cette variable est absente\u003C\u002Fli>\n  \u003Cli>Pool : 20 connexions maximum, timeout d'inactivité 60 s, durée de vie maximale 1 800 s, timeout de connexion 15 s\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Toutes les requêtes sont exécutées dans le schéma PostgreSQL dédié au vaisseau-mère.\u003C\u002Fp>\n\n\u003Ch3>La couche de traduction SQL\u003C\u002Fh3>\n\n\u003Cp>Chaque requête SQL est soumise à un pipeline de réécriture avant exécution. Les transformations sont appliquées dans l'ordre suivant :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>#\u003C\u002Fth>\n      \u003Cth>Transformation\u003C\u002Fth>\n      \u003Cth>Détail\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>1\u003C\u002Ftd>\n      \u003Ctd>Backticks → guillemets doubles\u003C\u002Ftd>\n      \u003Ctd>Conversion des délimiteurs d'identifiants MySQL vers la syntaxe PostgreSQL\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2\u003C\u002Ftd>\n      \u003Ctd>Qualification par schéma\u003C\u002Ftd>\n      \u003Ctd>Les tables préfixées par les espaces de noms internes sont automatiquement préfixées du nom de schéma PostgreSQL après les mots-clés \u003Ccode>FROM\u003C\u002Fcode>, \u003Ccode>JOIN\u003C\u002Fcode>, \u003Ccode>INTO\u003C\u002Fcode>, \u003Ccode>UPDATE\u003C\u002Fcode>, \u003Ccode>TABLE\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2b\u002F2c\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>DATE_SUB\u003C\u002Fcode> \u002F \u003Ccode>DATE_ADD\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Réécriture en arithmétique d'intervalles PostgreSQL, pour les unités DAY, MONTH, YEAR, HOUR, MINUTE, SECOND, sur littéraux comme sur placeholders\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2d\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>TIMESTAMPDIFF\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Converti en \u003Ccode>FLOOR(EXTRACT(EPOCH FROM …) \u002F diviseur)\u003C\u002Fcode> selon l'unité demandée\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>3\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>IFNULL\u003C\u002Fcode> → \u003Ccode>COALESCE\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>PostgreSQL ne connaît pas \u003Ccode>IFNULL\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>4\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>INSERT IGNORE\u003C\u002Fcode> → \u003Ccode>ON CONFLICT DO NOTHING\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Sans effet si \u003Ccode>ON CONFLICT\u003C\u002Fcode> est déjà présent\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>5\u003C\u002Ftd>\n      \u003Ctd>Auto-quotation des alias camelCase\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>AS fooBar\u003C\u002Fcode> → \u003Ccode>AS \"fooBar\"\u003C\u002Fcode> pour préserver la casse (PostgreSQL met en minuscules les identifiants non quotés). Exception : les types natifs PostgreSQL sont laissés tels quels pour ne pas interférer avec les expressions \u003Ccode>CAST(x AS TEXT)\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>6\u003C\u002Ftd>\n      \u003Ctd>Placeholders \u003Ccode>?\u003C\u002Fcode> → \u003Ccode>$1, $2, …\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Conversion positionnelle par un parseur caractère par caractère qui ignore les \u003Ccode>?\u003C\u002Fcode> présents à l'intérieur de chaînes quotées\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Sécurité des paramètres.\u003C\u002Fstrong> La transformation des placeholders ne fait que renommer les marqueurs dans le texte SQL. Le binding réel des valeurs est délégué à postgres-js via son mécanisme natif de requêtes paramétrées — aucune valeur n'est interpolée dans la chaîne SQL. Le tableau de paramètres est transmis tel quel, sans modification. Il n'y a donc aucun risque d'injection SQL sur les paramètres liés.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Cas non gérés automatiquement\u003C\u002Fstrong> (le code appelant doit les traiter manuellement) : \u003Ccode>ON DUPLICATE KEY UPDATE\u003C\u002Fcode>, \u003Ccode>LAST_INSERT_ID()\u003C\u002Fcode>, \u003Ccode>GROUP_CONCAT\u003C\u002Fcode>, \u003Ccode>FIND_IN_SET\u003C\u002Fcode>, \u003Ccode>DATE_FORMAT\u003C\u002Fcode>, \u003Ccode>CURDATE()\u003C\u002Fcode>. Lorsqu'un endpoint en a besoin, une branche conditionnelle dédiée est ajoutée côté appelant.\u003C\u002Fp>\n\n\u003Ch3>Interface et émulation de l'\u003Ccode>insertId\u003C\u002Fcode> MySQL\u003C\u002Fh3>\n\n\u003Cp>L'interface de l'adaptateur expose trois méthodes :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>\u003Ccode>query&lt;T&gt;(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — retourne un tableau de résultats typés\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>get&lt;T&gt;(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — retourne le premier résultat ou \u003Ccode>null\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>run(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — retourne un objet \u003Ccode>{ affectedRows, insertId }\u003C\u002Fcode> émulant le comportement MySQL\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>La méthode \u003Ccode>run()\u003C\u002Fcode> émule l'\u003Ccode>insertId\u003C\u002Fcode> MySQL : pour un \u003Ccode>INSERT\u003C\u002Fcode> sans clause \u003Ccode>RETURNING\u003C\u002Fcode> ni \u003Ccode>ON CONFLICT\u003C\u002Fcode>, elle ajoute automatiquement \u003Ccode>RETURNING id_&lt;entité&gt;\u003C\u002Fcode> en suivant la convention de nommage des clés primaires. Cette logique est désactivée pour les tables à clé primaire composite (une liste explicite dans le code) ainsi que pour toutes les tables suffixées \u003Ccode>_lang\u003C\u002Fcode> ou \u003Ccode>_shop\u003C\u002Fcode>, qui ne disposent pas d'une colonne \u003Ccode>id_&lt;table&gt;\u003C\u002Fcode> unique.\u003C\u002Fp>\n\u003Ch2 id=\"drizzle-schema-as-code\">Drizzle ORM — schéma-as-code pour la structure des tables\u003C\u002Fh2>\n\n\u003Cp>Les chemins décrits dans les sections précédentes (accès TypeScript côté Nuxt, accès Python côté outillage) font du \u003Cstrong>DML\u003C\u002Fstrong> : 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 \u003Cstrong>structure\u003C\u002Fstrong> (DDL — \u003Ccode>CREATE TABLE\u003C\u002Fcode>, colonnes, types, index, contraintes) est régie par un troisième chemin : \u003Cstrong>Drizzle ORM\u003C\u002Fstrong>, utilisé en mode \u003Cem>schema-as-code\u003C\u002Fem>. Les schémas TypeScript sont la \u003Cstrong>source de vérité déclarée\u003C\u002Fstrong> de la structure des tables ; le SQL \u003Ccode>CREATE\u003C\u002Fcode>\u002F\u003Ccode>ALTER\u003C\u002Fcode> est généré (ou rédigé) puis appliqué à la base de données, jamais l'inverse.\u003C\u002Fp>\n\n\u003Cp>Le dialecte était MariaDB avant la migration vers PostgreSQL effective lors d'un chantier de découplage antérieur. Les dépendances concernées sont \u003Ccode>drizzle-orm\u003C\u002Fcode> et \u003Ccode>drizzle-kit\u003C\u002Fcode> dans leurs versions courantes.\u003C\u002Fp>\n\n\u003Ch3>Configuration de Drizzle\u003C\u002Fh3>\n\n\u003Cp>Le fichier de configuration à la racine du dépôt définit les paramètres suivants :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Clé\u003C\u002Fth>\n      \u003Cth>Valeur\u003C\u002Fth>\n      \u003Cth>Note\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>dialect\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>'postgresql'\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Remplace l'ancien dialecte MariaDB\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>schema\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Trois globs couvrant le cœur OSS, les modules cockpit et les packs enterprise\u003C\u002Ftd>\n      \u003Ctd>Emplacements des déclarations TypeScript\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>out\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Dossier de migrations SQL générées, dans le cœur OSS\u003C\u002Ftd>\n      \u003Ctd>Destination des fichiers \u003Ccode>.sql\u003C\u002Fcode> produits\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>schemaFilter\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Schéma applicatif dédié\u003C\u002Ftd>\n      \u003Ctd>Restreint l'introspection\u002Fdiff à notre schéma, ignore \u003Ccode>public\u003C\u002Fcode> et les schémas natifs\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Credentials DB\u003C\u002Ftd>\n      \u003Ctd>Portés par variables d'environnement (hôte, port, utilisateur, mot de passe DB, nom de base)\u003C\u002Ftd>\n      \u003Ctd>Jamais écrits en clair dans le dépôt\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>strict\u003C\u002Fcode> \u002F \u003Ccode>verbose\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>true\u003C\u002Fcode> \u002F \u003Ccode>true\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Confirmation avant push, sortie détaillée\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ Le port par défaut configuré ici cible l'exposition TCP de la base de données sur la machine hôte (mapping host), et non le port interne du container. \u003Ccode>drizzle-kit\u003C\u002Fcode> est conçu pour pointer une base exposée en TCP, pas pour accéder directement au processus interne du container.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Commandes drizzle-kit\u003C\u002Fh3>\n\n\u003Cp>Aucun script npm dédié n'est exposé dans le manifeste du projet. Les commandes se lancent directement via \u003Ccode>npx\u003C\u002Fcode> :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Commande\u003C\u002Fth>\n      \u003Cth>Effet\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit generate\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Compare les schémas TS à l'état connu et écrit une nouvelle migration SQL dans le dossier de sortie\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit migrate\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Applique les migrations en attente à la base ciblée par les credentials\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit introspect\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Reverse-engineer une base existante vers des fichiers TypeScript (utile pour adopter une table legacy non encore déclarée)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Les schémas TypeScript\u003C\u002Fh3>\n\n\u003Cp>Chaque fichier déclare une ou plusieurs tables via l'API \u003Ccode>pgSchema(...).table(...)\u003C\u002Fcode> de Drizzle. Les schémas sont distribués en \u003Cstrong>trois périmètres\u003C\u002Fstrong> correspondant aux trois globs de configuration :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Périmètre\u003C\u002Fth>\n      \u003Cth>Contenu\u003C\u002Fth>\n      \u003Cth>Audité par le détecteur de dérive\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Cœur OSS + tables boutique\u003C\u002Ftd>\n      \u003Ctd>Tables historiques de la plateforme e-commerce (environ 85 fichiers, dont des re-exports)\u003C\u002Ftd>\n      \u003Ctd>✅ Oui\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Modules cockpit vaisseau-mère\u003C\u002Ftd>\n      \u003Ctd>Agents, chantiers, travaux, invoicing, drill… (environ 47 fichiers)\u003C\u002Ftd>\n      \u003Ctd>❌ Non\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Packs enterprise\u003C\u002Ftd>\n      \u003Ctd>Extensions métier avancées (environ 14 fichiers)\u003C\u002Ftd>\n      \u003Ctd>❌ Non\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Point d'attention :\u003C\u002Fstrong> un fichier de référence de typage définit la table du registre de modules, mais il est hors des trois globs actifs — il sert d'illustration et de référence de typage, pas comme entrée active du DDL Drizzle. Les tables cockpit sont déclarées dans les modules (périmètre 2) ; leurs migrations transitent par un système séparé (voir ci-dessous), pas par le DDL Drizzle.\u003C\u002Fp>\n\n\u003Cp>Le détecteur de dérive ne lit que le \u003Cstrong>périmètre cœur OSS\u003C\u002Fstrong>. Les colonnes déclarées dans les modules cockpit ou les packs enterprise ne déclenchent pas de drift bloquant — c'est pourquoi des colonnes ajoutées récemment peuvent coexister dans la base et dans la couche entité sans être présentes dans le schéma Drizzle du module.\u003C\u002Fp>\n\n\u003Cp>Ce que les schémas TypeScript typent : nom physique de colonne, type PostgreSQL, \u003Ccode>notNull\u003C\u002Fcode>, \u003Ccode>default\u003C\u002Fcode>\u002F\u003Ccode>defaultNow\u003C\u002Fcode>, \u003Ccode>primaryKey\u003C\u002Fcode> (simple ou composite), \u003Ccode>unique\u003C\u002Fcode>, index. Les types métier sont raffinés via \u003Ccode>$type&lt;...&gt;()\u003C\u002Fcode> (typage TS sans contrainte DB). Exemple de déclaration pour une table de registre de modules :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>export const appSchema = pgSchema('nom_du_schema_applicatif')\n\nexport type Runtime      = 'ps' | 'nuxt'\nexport type ModuleStatus = 'active' | 'disabled' | 'deprecated'\n\nexport const moduleRegistryTable = appSchema.table('nom_table_registre', {\n  idModuleRegistry: serial('id_module_registry').primaryKey(),\n  codename: varchar('codename', { length: 128 }).notNull().unique(),\n  version:  varchar('version',  { length: 32 }).notNull(),\n  runtime:  varchar('runtime', { length: 4 }).$type&lt;Runtime&gt;().notNull().default('ps'),\n  status:   varchar('status',  { length: 10 }).$type&lt;ModuleStatus&gt;().notNull().default('active'),\n  manifestJson: text('manifest_json').$type&lt;ModuleManifest | null&gt;(),\n  \u002F\u002F … hash de schéma, date de dernière migration, dateAdd, dateUpd\n}, (t) =&gt; ({\n  kRuntimeStatus: index('idx_runtime_status').on(t.runtime, t.status)\n}))\n\nexport type RegistryRow    = typeof moduleRegistryTable.$inferSelect\nexport type RegistryInsert = typeof moduleRegistryTable.$inferInsert\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Points à retenir :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Les \u003Cstrong>ENUM MariaDB\u003C\u002Fstrong> ont été portés en \u003Ccode>varchar(N) + $type&lt;Union&gt;()\u003C\u002Fcode> : la contrainte est au niveau TypeScript, pas sous forme de type ENUM PostgreSQL natif.\u003C\u002Fli>\n  \u003Cli>Les tables multilingues à clé primaire composite se traduisent en \u003Ccode>primaryKey({ columns: [t.idFaq, t.idLang] })\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Les types \u003Ccode>$inferSelect\u003C\u002Fcode>\u002F\u003Ccode>$inferInsert\u003C\u002Fcode> exportés donnent les types de ligne consommables côté code. Le runtime Nuxt passe par son propre adaptateur (accès DML direct), pas par le query-builder Drizzle — ici Drizzle ne sert qu'au DDL et au typage.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Les migrations générées\u003C\u002Fh3>\n\n\u003Cp>Le dossier de migrations du cœur OSS contient, à date, trois migrations idempotentes : ajout de colonnes logistiques, import mapping, et ajout d'une colonne téléphone sur la table client pour l'inscription B2B\u002FC. Le fichier journal de Drizzle (\u003Ccode>meta\u002F_journal.json\u003C\u002Fcode>) présente une \u003Cstrong>liste d'entrées vide\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ \u003Cstrong>Fait structurant :\u003C\u002Fstrong> le tracking d'application automatique de \u003Ccode>drizzle-kit migrate\u003C\u002Fcode> n'est \u003Cem>pas\u003C\u002Fem> la voie opérante. Les fichiers SQL du dossier de sortie sont écrits idempotents à la main (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>CREATE INDEX IF NOT EXISTS\u003C\u002Fcode>) et appliqués manuellement sur chaque base de données cliente. En pratique, Drizzle et les schémas TypeScript constituent la source de vérité \u003Cstrong>déclarative\u003C\u002Fstrong> ; l'application réelle reste une opération manuelle par base, jamais auto-propagée.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Deuxième voie de migrations (tables cockpit) :\u003C\u002Fstrong> les tables du cockpit vaisseau-mère — déclarées dans les modules, hors périmètre du détecteur de dérive — utilisent un système de migrations SQL manuelles séparé, réparti sur deux dossiers :\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>Un dossier dédié au schéma du vaisseau-mère (synedre.com, Odyssée, documentation, routage IA…). Les fichiers sont nommés par date et sujet, tous idempotents.\u003C\u002Fli>\n  \u003Cli>Un dossier racine pour les migrations appliquées directement à la base locale (tables cockpit hors vaisseau-mère). Ce dossier accueille aussi des sous-dossiers d'archivage (\u003Ccode>applied\u002F\u003C\u002Fcode>, \u003Ccode>_applied\u002F\u003C\u002Fcode>) qui tracent les migrations déjà exécutées sur la cible principale.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>L'application est manuelle dans les deux cas — ces migrations ne transitent pas par le mécanisme d'application automatique de dérive décrit ci-après.\u003C\u002Fp>\n\n\u003Ch3>Le détecteur de dérive TS ↔ base live\u003C\u002Fh3>\n\n\u003Cp>Comme l'application des migrations est manuelle et multi-tenant, un \u003Ccode>ALTER\u003C\u002Fcode> appliqué sur une base mais oublié sur une autre partirait en production silencieusement. Un script d'audit (environ 315 lignes) ferme ce trou.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Fonctionnement :\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>La fonction de parsing lit les fichiers TypeScript du périmètre cœur OSS et en extrait, par regex, la liste des tables et de leurs colonnes.\u003C\u002Fli>\n  \u003Cli>La fonction d'interrogation de base lit \u003Ccode>information_schema.columns\u003C\u002Fcode> pour le schéma applicatif concerné, restreinte aux tables du périmètre cœur, sur chaque base cible.\u003C\u002Fli>\n  \u003Cli>Le diff par base cible distingue : \u003Cstrong>bloquant\u003C\u002Fstrong> = table ou colonne déclarée en TS mais absente de la base (migration manquante) ; \u003Cstrong>info\u003C\u002Fstrong> = présente en base mais pas en TS.\u003C\u002Fli>\n  \u003Cli>Les faux positifs sont filtrés par un fichier d'exclusions : tables exclusives au vaisseau-mère à ignorer sur les bases clientes, et tables boutique OSS sorties de la base centrale lors d'un chantier de découplage antérieur.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Codes de sortie :\u003C\u002Fstrong> \u003Ccode>0\u003C\u002Fcode> — pas de dérive bloquante ; \u003Ccode>1\u003C\u002Fcode> — dérive bloquante détectée ; \u003Ccode>2\u003C\u002Fcode> — erreur d'exécution.\u003C\u002Fp>\n\n\u003Cp>L'audit s'utilise sur une base seule (mode par défaut, rétrocompatible), sur un tenant nommé, sur l'ensemble des tenants, ou avec affichage verbeux.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Gate de déploiement :\u003C\u002Fstrong> l'audit est câblé de façon \u003Cstrong>bloquante\u003C\u002Fstrong> dans le pipeline de déploiement. Avant de pousser un artefact, le pipeline vérifie que la base du tenant cible dispose bien de toutes les colonnes attendues par le code. En cas de dérive, le déploiement s'arrête. Il s'agit d'un check pré-déploiement ; il n'existe pas de cron périodique dédié.\u003C\u002Fp>\n\n\u003Ch3>L'applicateur automatique de dérive DDL\u003C\u002Fh3>\n\n\u003Cp>Un second script (environ 553 lignes) complète l'audit : là où l'audit \u003Cem>signale\u003C\u002Fem> la dérive, ce script la \u003Cem>corrige\u003C\u002Fem>.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Principes fondamentaux :\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>Jamais de \u003Ccode>DROP\u003C\u002Fcode>\u003C\u002Fstrong> (ni table, ni colonne). On n'ajoute que ce qui manque.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Idempotent :\u003C\u002Fstrong> rejouable sans effet si la base est déjà à jour.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Transaction unique\u003C\u002Fstrong> avec arrêt sur erreur : rollback total si une instruction échoue.\u003C\u002Fli>\n  \u003Cli>Génère uniquement des \u003Ccode>ADD COLUMN IF NOT EXISTS\u003C\u002Fcode> et \u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, avec types, contraintes \u003Ccode>NOT NULL\u003C\u002Fcode>\u002F\u003Ccode>DEFAULT\u003C\u002Fcode> et checks dérivés des schémas TypeScript.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Le script réutilise les définitions de l'auditeur (liste des cibles, parsers de schémas, chargement des exclusions) pour maintenir une source de vérité unique.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Pipeline interne :\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n  \u003Cli>Parsing des schémas TypeScript en structures \u003Ccode>TableDef\u003C\u002Fcode>\u002F\u003Ccode>ColumnDef\u003C\u002Fcode> (types PG, nullable, default, check).\u003C\u002Fli>\n  \u003Cli>Calcul de la dérive TS ↔ base live : tables absentes → \u003Ccode>CREATE TABLE\u003C\u002Fcode> ; colonnes absentes → \u003Ccode>ADD COLUMN\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Génération du bloc SQL idempotent.\u003C\u002Fli>\n  \u003Cli>Exécution en transaction sur la base cible (uniquement si le flag \u003Ccode>--apply\u003C\u002Fcode> est passé).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Mode d'usage\u003C\u002Fth>\n      \u003Cth>Effet\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Dry-run sur un tenant\u003C\u002Ftd>\n      \u003Ctd>Affiche le SQL sans l'exécuter (comportement par défaut)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Apply sur un tenant\u003C\u002Ftd>\n      \u003Ctd>Applique le SQL en transaction sur la base cible\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Dry-run sur tous les tenants\u003C\u002Ftd>\n      \u003Ctd>Inspecte l'ensemble des bases sans rien modifier\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Codes de sortie :\u003C\u002Fstrong> \u003Ccode>0\u003C\u002Fcode> — pas de dérive (ou application réussie) ; \u003Ccode>1\u003C\u002Fcode> — dérive détectée en dry-run, ou erreur lors de l'application ; \u003Ccode>2\u003C\u002Fcode> — erreur de parsing ou de connexion.\u003C\u002Fp>\n\n\u003Cp>L'applicateur est câblé dans le chemin de déploiement unifié, conditionné par un flag d'activation (\u003Ccode>DRIFT_AUTO_APPLY\u003C\u002Fcode>, désactivé par défaut — gate explicite). Depuis la consolidation des scripts de déploiement lors d'un chantier récent, tous les tenants empruntent ce chemin unifié ; les anciens scripts de déploiement per-tenant ont été supprimés, fermant le gap historique où certains tenants ne bénéficiaient pas de la correction automatique.\u003C\u002Fp>\n\n\u003Ch3>Qui fait foi pour l'évolution structurelle\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>Fichiers schéma TypeScript       ← SOURCE DE VÉRITÉ déclarative du DDL\n   │  (1) on édite le TS\n   ▼\nMigration SQL idempotente\n   │  (2a) Application manuelle par base impactée\n   │  (2b) Applicateur automatique de dérive (flag DRIFT_AUTO_APPLY=1)\n   ▼\nBase PostgreSQL live (vaisseau-mère + bases clientes)\n   ▲\n   └─ (3) Détecteur de dérive : vérifie TS == live, bloque le déploiement si écart\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Règle de reprise :\u003C\u002Fstrong> pour faire évoluer une table du périmètre cœur, on édite \u003Cem>d'abord\u003C\u002Fem> le schéma TypeScript, on génère ou rédige la migration idempotente, on l'applique sur chaque base (voie manuelle ou automatique), puis on relance l'audit — le pipeline de déploiement le rejouera de toute façon. Ne jamais modifier la base live sans répercuter le schéma TypeScript : l'audit le signalerait comme dérive bloquante au prochain déploiement.\u003C\u002Fp>\n\n\u003Cp>Inversement, les classes Entity Python utilisées par l'outillage ne créent jamais de colonne : leur liste de champs est une \u003Cem>whitelist\u003C\u002Fem> de colonnes supposées déjà présentes en base, pas une déclaration de structure.\u003C\u002Fp>\n\u003Ch2 id=\"acces-agentique-pattern-entity\">Accès agentique (Python) — le pattern Entity\u003C\u002Fh2>\n\n\u003Cp>Le code Python du vaisseau-mère n'accède pas à la base de données via un pool de connexions réseau comme le fait le runtime frontend. Il passe par un \u003Cstrong>mécanisme de sous-processus\u003C\u002Fstrong> qui délègue l'exécution SQL au container de base de données. Une classe de base commune centralise ce transport et expose un CRUD générique dont héritent une quarantaine de classes métier spécialisées.\u003C\u002Fp>\n\n\u003Ch3>Transport bas niveau\u003C\u002Fh3>\n\n\u003Cp>Trois helpers de la classe de base couvrent les cas d'usage courants :\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Helper\u003C\u002Fth>\n      \u003Cth>Usage\u003C\u002Fth>\n      \u003Cth>Notes\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Écriture sécurisée\u003C\u002Ftd>\n      \u003Ctd>INSERT \u002F UPDATE \u002F DELETE\u003C\u002Ftd>\n      \u003Ctd>Le SQL est écrit dans un fichier temporaire à nom unique (PID + UUID pour éviter les collisions multi-processus), copié dans le container, puis exécuté avec arrêt immédiat sur erreur (\u003Ccode>ON_ERROR_STOP=1\u003C\u002Fcode>). Aucun pipe stdin n'est utilisé.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Lecture tabulaire\u003C\u002Ftd>\n      \u003Ctd>SELECT sur colonnes simples\u003C\u002Ftd>\n      \u003Ctd>SQL transmis en ligne de commande, résultat séparé par tabulations. À éviter sur les colonnes TEXT contenant des sauts de ligne.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Lecture CSV\u003C\u002Ftd>\n      \u003Ctd>SELECT sur colonnes TEXT multi-lignes\u003C\u002Ftd>\n      \u003Ctd>Mode CSV (RFC 4180) ; les valeurs NULL sont restituées comme chaîne vide.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Chaque requête préfixe automatiquement la session avec le schéma applicatif approprié. Les paramètres de connexion (container cible, nom de base, utilisateur, mot de passe DB, schéma) sont lus depuis les variables d'environnement du processus — aucun identifiant n'est codé en dur.\u003C\u002Fp>\n\n\u003Ch3>La classe \u003Ccode>Entity\u003C\u002Fcode> — CRUD générique\u003C\u002Fh3>\n\n\u003Cp>La classe de base expose un CRUD paramétré par trois attributs de classe :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Nom de table\u003C\u002Fstrong> — la table physique ciblée.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Clé primaire\u003C\u002Fstrong> — nom de la colonne PK, utilisé dans les clauses \u003Ccode>RETURNING\u003C\u002Fcode> et \u003Ccode>WHERE\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Liste blanche de champs\u003C\u002Fstrong> — seules les colonnes déclarées ici sont acceptées en INSERT\u002FUPDATE ; toute colonne absente de la liste est silencieusement ignorée.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Le comportement standard de chaque méthode :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Création\u003C\u002Fstrong> : validation métier → injection automatique de \u003Ccode>client_id\u003C\u002Fcode> canonique si la colonne est dans la liste blanche → filtrage sur la liste blanche → \u003Ccode>INSERT … RETURNING &lt;pk&gt;\u003C\u002Fcode> avec horodatage \u003Ccode>NOW()\u003C\u002Fcode> sur \u003Ccode>date_add\u003C\u002Fcode> et \u003Ccode>date_upd\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Mise à jour\u003C\u002Fstrong> : filtrage sur la liste blanche + mise à jour de \u003Ccode>date_upd\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Recherche \u002F existence \u002F suppression\u003C\u002Fstrong> : implémentations standard.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>L'échappement SQL interne gère nativement les booléens (\u003Ccode>TRUE\u003C\u002Fcode>\u002F\u003Ccode>FALSE\u003C\u002Fcode>), les dictionnaires et les listes (sérialisés en JSON).\u003C\u002Fp>\n\n\u003Cp>Les sous-classes surchargent la méthode \u003Ccode>validate(data, mode)\u003C\u002Fcode> pour y placer les règles métier : les violations bloquantes lèvent une \u003Ccode>ValidationError\u003C\u002Fcode> ; les avertissements non bloquants sont collectés et retournés à l'appelant sans interrompre l'opération.\u003C\u002Fp>\n\n\u003Ch2 id=\"creation-atomique-chantier\">Création atomique d'un chantier avec son squelette\u003C\u002Fh2>\n\n\u003Cp>Pour interdire les chantiers orphelins (sans travail ni tâche), la méthode de création du composant \u003Cem>Chantier\u003C\u002Fem> crée en une seule transaction \u003Ccode>BEGIN … COMMIT\u003C\u002Fcode> :\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>Le \u003Cstrong>chantier\u003C\u002Fstrong> lui-même.\u003C\u002Fli>\n  \u003Cli>Un \u003Cstrong>premier travail\u003C\u002Fstrong> rattaché au chantier (lookup par codename).\u003C\u002Fli>\n  \u003Cli>Une ou plusieurs \u003Cstrong>tâches initiales\u003C\u002Fstrong> rattachées au travail (lookup par identifiant de travail).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>L'option \u003Ccode>ON_ERROR_STOP=1\u003C\u002Fcode> garantit le rollback complet si l'une quelconque des insertions échoue.\u003C\u002Fp>\n\n\u003Ch3>Validations bloquantes\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>Le codename doit respecter le format kebab-case (4 à 64 caractères) et être unique.\u003C\u002Fli>\n  \u003Cli>La priorité doit appartenir à l'ensemble \u003Ccode>{P0, P1, P2, P3}\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Chaque tâche doit avoir un titre et un assigné non vides ; l'assigné doit exister dans le référentiel des agents.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Recrutement multi-agents\u003C\u002Fstrong> : pour tout chantier dont le périmètre concerne un tenant client, au moins deux agents distincts doivent être assignés — règle issue de la doctrine de supervision croisée.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Comportements automatiques\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>Les champs \u003Ccode>priority\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>estimated_tokens\u003C\u002Fcode>, \u003Ccode>estimated_h\u003C\u002Fcode>, \u003Ccode>position\u003C\u002Fcode>, \u003Ccode>recommended_model\u003C\u002Fcode>, \u003Ccode>scope\u003C\u002Fcode>, \u003Ccode>visual_intent\u003C\u002Fcode> et \u003Ccode>visual_url\u003C\u002Fcode> sont propagés en transparence depuis le chantier vers chaque tâche.\u003C\u002Fli>\n  \u003Cli>Si \u003Ccode>recommended_model\u003C\u002Fcode> est absent d'une tâche, il est calculé automatiquement par l'heuristique de sélection de modèle (voir ci-dessous).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"entite-tache-estimation-modele\">Entité Tâche — estimation, modèle recommandé, compétences et outils\u003C\u002Fh2>\n\n\u003Ch3>Estimation automatique\u003C\u002Fh3>\n\n\u003Cp>Lors de la création d'une tâche, si le volume de tokens n'est pas fourni, un estimateur dédié est appelé automatiquement. Un avertissement non bloquant est émis si l'estimation reste manquante.\u003C\u002Fp>\n\n\u003Ch3>Heuristique de sélection du modèle IA\u003C\u002Fh3>\n\n\u003Cp>La méthode de recommandation de modèle suit une cascade de critères :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Modèle le plus puissant\u003C\u002Fstrong> si la priorité est P0, ou si la tâche a échoué de façon récurrente (≥ 2 itérations en échec), ou si le volume estimé dépasse 8 000 tokens.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Modèle intermédiaire\u003C\u002Fstrong> si le volume est compris entre 1 500 et 8 000 tokens.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Modèle léger\u003C\u002Fstrong> pour les tâches en dessous de 1 500 tokens sans facteur aggravant.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Attachement de compétences et d'outils\u003C\u002Fh3>\n\n\u003Cp>Deux méthodes permettent de qualifier une tâche :\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Compétence\u003C\u002Fstrong> : recherche dans le référentiel de compétences par nom (clé naturelle) → insertion dans la table de liaison avec clause \u003Ccode>ON CONFLICT … DO NOTHING\u003C\u002Fcode>. Si la compétence est inconnue, une proposition est enregistrée en statut \u003Cem>pending\u003C\u002Fem> et la méthode retourne \u003Ccode>False\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Outil\u003C\u002Fstrong> : même logique via le référentiel d'outils (recherche par slug). Outil inconnu → proposition \u003Cem>pending\u003C\u002Fem> + retour \u003Ccode>False\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"cascades-de-statut\">Cascades de statut\u003C\u002Fh2>\n\n\u003Cp>Les mises à jour de statut sur les tâches et les travaux déclenchent des cascades automatiques vers les niveaux supérieurs :\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Tâche → done \u002F cancelled\n  (condition : toutes les tâches du travail sont dans un état terminal,\n               dont au moins une en done)\n  └─ Cascade vers le Travail\n       ├─ Une équipe QA est recrutée ?\n       │    └─ Oui → le verdict QA est appliqué (run de validation)\n       └─ Non → Travail passe à done\n\nTravail → done \u002F cancelled\n  (condition : tous les travaux du chantier sont terminaux,\n               dont au moins un en done)\n  └─ Cascade vers le Chantier\n       ├─ Garde-fou discovery-only :\n       │    tous les travaux terminés en phase discovery\n       │    → explosion automatique (un agent LLM génère les travaux d'implémentation)\n       │    au lieu de promouvoir le chantier\n       └─ Sinon → Chantier passe en statut test (préprod, en attente de revue)\n            └─ Avertissement non bloquant si le plan de test préprod\n               ou la commande de livraison sont manquants\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Résolution automatique de travaux bloqués\u003C\u002Fh3>\n\n\u003Cp>Un travail correctif peut être lié à un travail parent en pause. Quand le travail correctif passe à \u003Ccode>done\u003C\u002Fcode>, le travail parent est automatiquement fermé : son statut passe de \u003Ccode>paused\u003C\u002Fcode> à \u003Ccode>done\u003C\u002Fcode>, ses tâches restantes passent à \u003Ccode>cancelled\u003C\u002Fcode>, et la décision est consignée dans le journal d'audit du travail parent.\u003C\u002Fp>\n\n\u003Ch2 id=\"conventions-i18n\">Conventions d'internationalisation et polymorphisme\u003C\u002Fh2>\n\n\u003Ch3>Tables de traduction (\u003Ccode>_lang\u003C\u002Fcode>)\u003C\u002Fh3>\n\n\u003Cp>Tout texte visible par un visiteur vit dans une table sœur suffixée \u003Ccode>_lang\u003C\u002Fcode>, jamais dans la table parente.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Le suffixe est exactement \u003Ccode>_lang\u003C\u002Fcode> — pas \u003Ccode>_translation\u003C\u002Fcode>, \u003Ccode>_i18n\u003C\u002Fcode> ni \u003Ccode>_locale\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>La clé primaire est \u003Cstrong>composite\u003C\u002Fstrong> : \u003Ccode>(id_&lt;entité&gt;, id_lang)\u003C\u002Fcode>, sans auto-incrément. En contexte multi-boutique, la PK devient \u003Ccode>(id_&lt;entité&gt;, id_lang, id_shop)\u003C\u002Fcode> — aucune table \u003Ccode>_shop_lang\u003C\u002Fcode> séparée n'est créée.\u003C\u002Fli>\n  \u003Cli>Séparation stricte des responsabilités : la table parente porte les clés étrangères, flags, dates et énumérations ; la table \u003Ccode>_lang\u003C\u002Fcode> porte les champs textuels (\u003Ccode>title\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>meta_*\u003C\u002Fcode> et tout texte orienté visiteur).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Cette convention a un impact direct sur l'adaptateur de requêtes : les tables \u003Ccode>_lang\u003C\u002Fcode> sont exclues de l'heuristique de récupération de clé primaire car elles ne possèdent pas de PK simple. Côté frontend, toutes les chaînes passent par la fonction de traduction ; aucune chaîne UI ne doit être codée en dur dans le code source.\u003C\u002Fp>\n\n\u003Ch3>Polymorphisme par \u003Ccode>parent_type\u003C\u002Fcode> \u002F \u003Ccode>parent_id\u003C\u002Fcode>\u003C\u002Fh3>\n\n\u003Cp>Quand une fonctionnalité s'applique à plusieurs types d'entités parentes, une \u003Cstrong>seule table polymorphique\u003C\u002Fstrong> est créée — jamais une table par type de parent. La colonne \u003Ccode>parent_type\u003C\u002Fcode> porte un discriminant textuel (\u003Ccode>'cms'\u003C\u002Fcode>, \u003Ccode>'category'\u003C\u002Fcode>, \u003Ccode>'product'\u003C\u002Fcode>…) et \u003Ccode>parent_id\u003C\u002Fcode> l'identifiant dans la table correspondante.\u003C\u002Fp>\n\n\u003Cp>Exception documentée : l'\u003Cstrong>extension 1:1 d'une entité native\u003C\u002Fstrong> adopte le pattern de table extra (PK = FK vers l'entité native), sans polymorphisme. Les tables de liaison pure N:N sont nommées en ordre alphabétique des deux entités, sans suffixe \u003Ccode>_asso\u003C\u002Fcode> ni \u003Ccode>_link\u003C\u002Fcode>, et sans table \u003Ccode>_lang\u003C\u002Fcode> associée.\u003C\u002Fp>\n\n\u003Ch3>Pas de JSON métier en colonne\u003C\u002Fh3>\n\n\u003Cp>Les colonnes de contenu JSON (\u003Ccode>payload_json\u003C\u002Fcode>, \u003Ccode>content_i18n\u003C\u002Fcode>, \u003Ccode>labels_json\u003C\u002Fcode>…) sont \u003Cstrong>interdites\u003C\u002Fstrong> pour du contenu métier structuré. La seule tolérance concerne les \u003Cstrong>payloads techniques éphémères\u003C\u002Fstrong> (webhooks, logs, état de session) documentés explicitement par un commentaire de colonne.\u003C\u002Fp>\n\n\u003Cp>Dans le cockpit agentique, les colonnes JSON sont réservées à l'audit-trail technique en mode append-only (contexte de décision, découvertes, historique d'itération) — c'est la tolérance, pas la règle générale. Un audit de schéma automatisé (boucle nocturne planifiée) détecte et signale les violations en priorité P0.\u003C\u002Fp>\n\n\u003Ch2 id=\"pieges-de-reprise\">Pièges courants à l'intégration\u003C\u002Fh2>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>Vues-shim vs tables physiques\u003C\u002Fstrong> : plusieurs noms d'entités exposés dans l'API sont en réalité des \u003Cem>vues\u003C\u002Fem> qui relisent des tables physiques sous un autre nom. En lecture, la vue suffit ; en écriture, il faut cibler la table physique sous-jacente. Ce pattern concerne une douzaine de paires (agents, activité d'agent, heartbeat, relations, XP, historique XP, automates, agents d'automate, conduites, logs d'automate, logs de règles intelligentes).\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Container ≠ base ≠ schéma\u003C\u002Fstrong> : le container de base de données, le nom logique de la base et le schéma applicatif sont trois niveaux distincts. Les confondre produit des erreurs de résolution silencieuses.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Tenant non migré PostgreSQL\u003C\u002Fstrong> : l'adaptateur de requêtes lève une erreur immédiate pour tout tenant non déclaré dans la liste des tenants PostgreSQL activés. Le chemin de secours MySQL a été supprimé.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Énumération \u003Ccode>scope\u003C\u002Fcode>\u003C\u002Fstrong> : la contrainte CHECK en base de données est l'unique référence de vérité pour les valeurs autorisées du champ \u003Ccode>scope\u003C\u002Fcode> — elle peut être plus large que ce qu'indique la documentation écrite.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Résidu de renommage\u003C\u002Fstrong> : la table des tâches a conservé ses séquences et contraintes nommées selon l'ancien préfixe, résidu d'un renommage partiel. Ce n'est pas une erreur — c'est un état connu.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>SQL MySQL legacy\u003C\u002Fstrong> : un convertisseur automatique transforme les requêtes MySQL vers PostgreSQL, mais plusieurs constructions ne sont pas couvertes (\u003Ccode>GROUP_CONCAT\u003C\u002Fcode>, \u003Ccode>DATE_FORMAT\u003C\u002Fcode>, \u003Ccode>ON DUPLICATE KEY UPDATE\u003C\u002Fcode>…). Ces cas échouent silencieusement et nécessitent une branche PostgreSQL native écrite manuellement.\n  \u003C\u002Fli>\n\u003C\u002Fol>","\u003Ch2 id=\"data-layer-overview\">The Synedre OS Data Layer\u003C\u002Fh2>\n\n\u003Cp>This page describes how the Synedre OS agentic harness reads and writes its data: a central PostgreSQL database, an adapter that translates SQL queries on the web interface side, Python classes on the agentic tooling side, a schema-as-code system for migrations, and the naming conventions that hold everything together. It is intended for engineers taking over the codebase.\u003C\u002Fp>\n\n\u003Ch2 id=\"db-unique\">1. The Central Database\u003C\u002Fh2>\n\n\u003Cp>All mothership data lives in \u003Cstrong>a single PostgreSQL schema\u003C\u002Fstrong>, hosted in a dedicated container on the mothership VPS. The database and the schema carry distinct names — a point that often surprises newcomers: the container, the database, and the schema each have a different identifier, and the harness tables live in the specific schema, not in the \u003Ccode>public\u003C\u002Fcode> schema.\u003C\u002Fp>\n\n\u003Cp>Access credentials (DB user, password) are carried by environment files and are never written in plain text in the code; the application raises an explicit error if they are absent.\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Harness \u002F multi-tenant boundary.\u003C\u002Fstrong> This chapter documents exclusively the central schema of the \u003Cstrong>Synedre OS harness\u003C\u002Fstrong> (private agentic cockpit, single-database). The CodeMyShop tenant databases use the same connection adapter (see §4–5) but their data never crosses the harness schema: each tenant has its own database, its own schema, its own tables. §4 covers the adapter because it also serves the cockpit — but any reference to tenants there is contextual, not constitutive of the harness.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>Three Access Paths\u003C\u002Fh3>\n\n\u003Cp>Two paths coexist at runtime (row-level read\u002Fwrite — DML); a third operates at the structural level (schema evolution — DDL):\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>                   ┌──────────────────────────────────────┐\n                   │         PostgreSQL — central database │\n                   │         Synedre OS harness schema     │\n                   └───────────────┬──────────────────────┘\n                                   │\n   ┌──────────────────┬────────────┴──────────────┬─────────────────────┐\n   │  DML — runtime   │  DML — tooling            │  DDL — structure    │\n   │ (A) Web          │ (B) Agentic Python         │ (C) Schema-as-code  │\n   │     interface    │     tooling                │     (Drizzle ORM)   │\n   │     (Nuxt)       │  execution via container   │  generated and      │\n   │  TCP pool        │  (temporary SQL file       │  verified SQL       │\n   │  postgres-js     │   or inline query)         │  migrations (§6)    │\n   └──────────────────┴────────────────────────────┴─────────────────────┘\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Path A\u003C\u002Fstrong> — the web interface (Nuxt\u002FNitro) accesses the database via a TCP connection pool (\u003Cem>postgres-js\u003C\u002Fem>). A central utility exposes the connection; a dedicated adapter handles compatibility with legacy queries (see §4–5).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Path B\u003C\u002Fstrong> — the agentic Python tooling executes queries through a container-based execution mechanism: writes go through a temporary SQL file that is copied and then executed; reads use an inline query. Neither path uses a stdin pipe.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Path C\u003C\u002Fstrong> — DDL is managed as \u003Cem>schema-as-code\u003C\u002Fem> (Drizzle ORM): this is the authoritative source for structural table evolution. It does not manipulate rows; it generates and applies SQL migrations (detailed in §6).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Table Family Snapshot (May 31, 2026)\u003C\u002Fh3>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Family\u003C\u002Fth>\n      \u003Cth>Volume (base tables)\u003C\u002Fth>\n      \u003Cth>Scope\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Legacy private CodeMyShop \u002F mothership tables\u003C\u002Ftd>\n      \u003Ctd>159\u003C\u002Ftd>\n      \u003Ctd>Migration debt — cockpit, agents, scars…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Synedre OS cockpit tables\u003C\u002Ftd>\n      \u003Ctd>69\u003C\u002Ftd>\n      \u003Ctd>Agents, projects, runs, negotiation…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Native PrestaShop tables\u003C\u002Ftd>\n      \u003Ctd>18\u003C\u002Ftd>\n      \u003Ctd>Historical debt — products, categories, translations…\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Public PaaS tables (tenants)\u003C\u002Ftd>\n      \u003Ctd>≈ 1 in this schema\u003C\u002Ftd>\n      \u003Ctd>Almost absent from the harness schema; lives primarily in tenant DBs\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>In addition, there are \u003Cstrong>16 views\u003C\u002Fstrong> in the central schema (see §3.4 — critical point: the agents view is a projection of the corresponding cockpit table).\u003C\u002Fp>\n\n\u003Cblockquote>\n\u003Cp>The exact count evolves with each project; the figures above are the snapshot from May 31, 2026. The canonical truth query is documented in §7.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch2 id=\"familles-tables\">2. Table Families and Their Conventions\u003C\u002Fh2>\n\n\u003Cp>A table's prefix encodes its scope and ownership regime. Four families coexist in the ecosystem:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Family\u003C\u002Fth>\n      \u003Cth>Scope\u003C\u002Fth>\n      \u003Cth>Status\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Public PaaS OSS tables\u003C\u002Ftd>\n      \u003Ctd>Community core — FAQ, welcome blocks, etc.\u003C\u002Ftd>\n      \u003Ctd>Target (in tenant DBs)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Synedre OS cockpit tables\u003C\u002Ftd>\n      \u003Ctd>Agents, projects, runs, negotiation, atomic tasks\u003C\u002Ftd>\n      \u003Ctd>Target\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Legacy private CodeMyShop tables\u003C\u002Ftd>\n      \u003Ctd>Former mothership and cockpit scope\u003C\u002Ftd>\n      \u003Ctd>Migration debt\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Native PrestaShop tables\u003C\u002Ftd>\n      \u003Ctd>Products, categories, translations…\u003C\u002Ftd>\n      \u003Ctd>Historical debt\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>Cross-Cutting Conventions\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Singular name\u003C\u002Fstrong> — a table is named in the singular (aligned with the native PrestaShop convention).\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>One table = one parent entity\u003C\u002Fstrong> — no catch-all tables.\u003C\u002Fli>\n  \u003Cli>Source files are named in \u003Cstrong>kebab-case\u003C\u002Fstrong>; Vue components in \u003Cstrong>PascalCase\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Migration history.\u003C\u002Fstrong> The public PaaS table family results from a one-time rename performed in v0.2.0, from the former legacy prefix (at the time when CodeMyShop was a PrestaShop extension). In the harness schema, the majority of tables remained under the old prefix — the debt has not yet been settled — while the new PaaS prefix is applied in the OSS tenant databases.\u003C\u002Fp>\n\n\u003Ch3 id=\"cohabitation-chantier\">2.1 Family Cohabitation on a Single Logical Entity\u003C\u002Fh3>\n\n\u003Cp>A point that often surprises newcomers: a single logical entity — the \u003Cstrong>project\u003C\u002Fstrong> — is spread across both the legacy and cockpit families.\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Table role\u003C\u002Fth>\n      \u003Cth>Family\u003C\u002Fth>\n      \u003Cth>Granularity\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>The project itself\u003C\u002Ftd>\n      \u003Ctd>Legacy private\u003C\u002Ftd>\n      \u003Ctd>1 row per project\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Granular work items\u003C\u002Ftd>\n      \u003Ctd>Legacy private\u003C\u002Ftd>\n      \u003Ctd>N rows per project\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Atomic tasks\u003C\u002Ftd>\n      \u003Ctd>Synedre OS cockpit\u003C\u002Ftd>\n      \u003Ctd>N rows per work item\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Cockpit satellites (team, multi-session lock, QA…)\u003C\u002Ftd>\n      \u003Ctd>Synedre OS cockpit\u003C\u002Ftd>\n      \u003Ctd>N rows per project\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Historical artifact.\u003C\u002Fstrong> The atomic tasks table was renamed during the migration to the cockpit family, but its primary key sequence and the PRIMARY KEY constraint retain the old family name. The rename affected the table name, not all dependent objects — to be corrected in the next DDL normalization project.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"modele-donnees-tables-cles\">Data Model of Key Tables\u003C\u002Fh2>\n\n\u003Ch3>The Projects Table\u003C\u002Fh3>\n\n\u003Cp>The central projects table uses a serial primary key. Its notable columns are as follows:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>codename\u003C\u002Fstrong> — unique kebab-case identifier for the project (varchar 64, required)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>title\u003C\u002Fstrong> — human-readable label (varchar 255, required)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>client_id\u003C\u002Fstrong> — optional reference to a client; \u003Ccode>NULL\u003C\u002Fcode> indicates an internal project\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>status\u003C\u002Fstrong> — current state, default \u003Ccode>'planning'\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>priority\u003C\u002Fstrong> — priority level, default \u003Ccode>'P2'\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>current_focus\u003C\u002Fstrong>, \u003Cstrong>deadline\u003C\u002Fstrong>, \u003Cstrong>notes\u003C\u002Fstrong>, \u003Cstrong>mission_letter\u003C\u002Fstrong>, \u003Cstrong>preprod_test_plan\u003C\u002Fstrong> — steering text fields\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>external_contacts\u003C\u002Fstrong> — third-party contacts attached to the project\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>ship_command\u003C\u002Fstrong> — associated delivery command (varchar 255)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>scope\u003C\u002Fstrong> — scope constrained by an enum: \u003Ccode>synedre\u003C\u002Fcode>, \u003Ccode>codemyshop-oss\u003C\u002Fcode>, \u003Ccode>codemyshop-enterprise\u003C\u002Fcode>, \u003Ccode>tenant\u003C\u002Fcode>, \u003Ccode>business\u003C\u002Fcode>, \u003Ccode>juridique\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>, \u003Ccode>conseil\u003C\u002Fcode> (or \u003Ccode>NULL\u003C\u002Fcode>). This database-level enum is broader than the one documented in the root reference file, which does not list the values \u003Ccode>juridique\u003C\u002Fcode>, \u003Ccode>negociation\u003C\u002Fcode>, and \u003Ccode>conseil\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>auto_explode\u003C\u002Fstrong> — boolean, default \u003Ccode>true\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>mode_auto\u003C\u002Fstrong> — boolean, default \u003Ccode>false\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>max_cost_eur\u003C\u002Fstrong> — budget ceiling in euros\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>archived_at\u003C\u002Fstrong> \u002F \u003Cstrong>archived_by\u003C\u002Fstrong> — archival timestamp and author\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>date_add\u003C\u002Fstrong> \u002F \u003Cstrong>date_upd\u003C\u002Fstrong> — creation and update timestamps (\u003Ccode>timestamptz\u003C\u002Fcode>, default \u003Ccode>now()\u003C\u002Fcode>)\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>An active trigger on this table fires after any update to the \u003Ccode>status\u003C\u002Fcode> column and propagates the resolution to the inbound e-mail log entries linked to the project. This cascade mechanism ensures consistency between the project state and the items in its associated inbox.\u003C\u002Fp>\n\n\u003Ch3>The Tasks Table\u003C\u002Fh3>\n\n\u003Cp>Each project is broken down into tasks. The tasks table is linked to the project via a logical foreign key to the work items table. Key columns:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>title\u003C\u002Fstrong>, \u003Cstrong>status\u003C\u002Fstrong> (default \u003Ccode>'todo'\u003C\u002Fcode>), \u003Cstrong>priority\u003C\u002Fstrong> (default \u003Ccode>'P2'\u003C\u002Fcode>)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>assignee_codename\u003C\u002Fstrong> — codename of the assigned agent (varchar 64)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>estimated_tokens\u003C\u002Fstrong> \u002F \u003Cstrong>actual_tokens\u003C\u002Fstrong> \u002F \u003Cstrong>actual_cost_usd\u003C\u002Fstrong> — AI consumption metrics\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>recommended_model\u003C\u002Fstrong> — AI model recommended for this task (varchar 32)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>position\u003C\u002Fstrong> — display order\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>scope\u003C\u002Fstrong> — constrained scope: \u003Ccode>synedre-internal\u003C\u002Fcode>, \u003Ccode>codemyshop-oss\u003C\u002Fcode>, \u003Ccode>codemyshop-enterprise\u003C\u002Fcode>, \u003Ccode>tenant-single\u003C\u002Fcode>, \u003Ccode>tenant-multi\u003C\u002Fcode>, \u003Ccode>infra\u003C\u002Fcode>, \u003Ccode>doctrine\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>visual_intent\u003C\u002Fstrong> (\u003Ccode>text\u003C\u002Fcode>, nullable) — description of what should be visible on screen after the change; \u003Ccode>NULL\u003C\u002Fcode> indicates a non-visual task. This field feeds the automated visual verification engine.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>visual_url\u003C\u002Fstrong> (\u003Ccode>text\u003C\u002Fcode>, nullable) — verification URL; \u003Ccode>NULL\u003C\u002Fcode> falls back to the project's staging environment\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The \u003Ccode>visual_intent\u003C\u002Fcode> and \u003Ccode>visual_url\u003C\u002Fcode> columns were added via an idempotent migration (\u003Ccode>ADD COLUMN IF NOT EXISTS\u003C\u002Fcode>). They are recognised by the Python entity management layer but are \u003Cstrong>not yet\u003C\u002Fstrong> reflected in the TypeScript schema on the web application side — a deliberate divergence between the live database and the application schema, outside the scope of the automatic drift audit tool, which only reads the main schema directory.\u003C\u002Fp>\n\n\u003Cp>The tasks table is also referenced by an inter-task dependency table, enabling the modelling of a scheduling graph.\u003C\u002Fp>\n\n\u003Ch3>The Learning Log (Scars and Victories)\u003C\u002Fh3>\n\n\u003Cp>This log records errors, lessons learned, and victories from the agent team. Each entry carries:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>agent_codename\u003C\u002Fstrong> — codename of the agent concerned (required)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>error_type\u003C\u002Fstrong>, \u003Cstrong>description\u003C\u002Fstrong> (required), \u003Cstrong>root_cause\u003C\u002Fstrong>, \u003Cstrong>corrected_by\u003C\u002Fstrong>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>severity\u003C\u002Fstrong> — severity level: \u003Ccode>low\u003C\u002Fcode>, \u003Ccode>medium\u003C\u002Fcode>, \u003Ccode>high\u003C\u002Fcode>, \u003Ccode>critical\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>kind\u003C\u002Fstrong> — entry type: \u003Ccode>'failure'\u003C\u002Fcode> (default) or \u003Ccode>'victory'\u003C\u002Fcode> (created via the dedicated skill)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>resolved\u003C\u002Fstrong> — resolution state (integer values: 0, 1, or 2)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>tags\u003C\u002Fstrong> — array of keywords (\u003Ccode>text[]\u003C\u002Fcode>)\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>importance\u003C\u002Fstrong> — score from 1 to 10\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>recall_count\u003C\u002Fstrong>, \u003Cstrong>learnable\u003C\u002Fstrong> — pedagogical reusability indicators\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>This log is for internal use only: it has no language column and is not exposed directly to end users.\u003C\u002Fp>\n\n\u003Ch3>Agents: A Compatibility View, Not a Physical Table\u003C\u002Fh3>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Major pitfall when taking over the codebase.\u003C\u002Fstrong> The physical agents table is distinct from the historically exposed view. The view is a backwards-compatibility \u003Cem>shim\u003C\u002Fem> resulting from a prior consolidation migration.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>The physical agents table contains the columns \u003Ccode>codename\u003C\u002Fcode>, \u003Ccode>nickname\u003C\u002Fcode>, \u003Ccode>role\u003C\u002Fcode>, \u003Ccode>group_name\u003C\u002Fcode>, \u003Ccode>active\u003C\u002Fcode>, workstation configuration fields (\u003Ccode>job_*\u003C\u002Fcode>), \u003Ccode>cognitive_frame\u003C\u002Fcode>, and \u003Ccode>heritage\u003C\u002Fcode>. The view exposed under the legacy name is a simple \u003Ccode>SELECT … FROM\u003C\u002Fcode> projection of this physical table. Any read through the legacy name works transparently; however, \u003Cstrong>writes must target the physical table\u003C\u002Fstrong> — views of this type are likely read-only. Verify before any \u003Ccode>UPDATE\u003C\u002Fcode> or \u003Ccode>INSERT\u003C\u002Fcode> through the legacy name.\u003C\u002Fp>\n\n\u003Cp>The same view-shim mechanism covers approximately a dozen similar pairs: automata, agent activities, heartbeats, agent relationships, XP and their history, automata and their routines, their logs, and the intelligent steering tables. In total, sixteen views coexist in the database schema, distributed as follows:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Twelve shim views\u003C\u002Fstrong> — transparent redirection from the legacy namespace to the new one\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>One event view\u003C\u002Fstrong> — linked to the Atlas spawn mechanism\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Three SRE analytical views\u003C\u002Fstrong>:\n    \u003Cul>\n      \u003Cli>Daily aggregation of the learning log over 14 days, broken down by severity level and entry type\u003C\u002Fli>\n      \u003Cli>Summary of review runs over 14 days: total count, rollback count, rollback rate, number of blocking verdicts, last run\u003C\u002Fli>\n      \u003Cli>Warning frequency over 30 days: count by warning code, number of associated blocking verdicts, last occurrence\u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Error-tracking view not instantiated.\u003C\u002Fstrong> A unified error view is referenced in the error-tracking module manifest and called by the API code, but it \u003Cstrong>does not exist in the live database\u003C\u002Fstrong>: one of its two sources (PostHog errors) is not instantiated in the current schema. Only the server-error source is present. A prior migration was therefore unable to recreate this view. The actual view count is \u003Cstrong>16\u003C\u002Fstrong>, not 17.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Chr>\n\n\u003Ch2 id=\"acces-runtime-multi-tenant\">Runtime Database Access — Multi-Tenant\u003C\u002Fh2>\n\n\u003Ch3>Client Resolution and Adapter Selection\u003C\u002Fh3>\n\n\u003Cp>The database access module exposes a main function that, given an incoming HTTP request, identifies the relevant tenant and returns a ready-to-use connection adapter.\u003C\u002Fp>\n\n\u003Cp>Tenant resolution follows a three-rule cascade, in priority order:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>A client identifier explicitly set in the process runtime configuration (each client VPS may define it statically).\u003C\u002Fli>\n  \u003Cli>A match between the request hostname and a tenant-to-database mapping table, enabling multiple tenants to be served from a single application instance.\u003C\u002Fli>\n  \u003Cli>A fallback to the internal identifier of the mothership (\u003Ccode>ac-hub\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>Once the tenant is identified, a second function determines whether the PostgreSQL connection is enabled for that tenant. Two whitelists coexist: mothership-internal tenants (a list hardcoded in the source) and PostgreSQL-enabled tenants dynamically activated via an environment variable. If the tenant belongs to neither list, the function raises an exception — the MySQL path was permanently removed during a prior migration phase.\u003C\u002Fp>\n\n\u003Ch3>Global PostgreSQL Lock\u003C\u002Fh3>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Critical point of vigilance.\u003C\u002Fstrong> A global lock, controlled by the environment variable \u003Ccode>PG_ENABLED_DOMAINS\u003C\u002Fcode>, gates PostgreSQL access for \u003Cem>all\u003C\u002Fem> tenants without exception. If this variable does not contain the wildcard \u003Ccode>*\u003C\u002Fcode>, even a tenant present in the internal whitelist will be rejected. In production, the global switch is in place (\u003Ccode>*\u003C\u002Fcode>) and the lock is open. However, clearing this variable constitutes a full rollback to MariaDB for the entire system — an operation to be handled with extreme caution.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>This mechanism is a legacy of the progressive migration strategy: in the early phases, only certain modules were enabled on PostgreSQL (\u003Ccode>PG_ENABLED_DOMAINS=cms,inventory\u003C\u002Fcode>); the global switch (\u003Ccode>=*\u003C\u002Fcode>) was applied only at the final cutover phase.\u003C\u002Fp>\n\n\u003Ch3>Tenant-to-Database Mapping Table\u003C\u002Fh3>\n\n\u003Cp>The mapping table is built dynamically from environment variables following the convention \u003Ccode>NUXT_TENANT_DB_&lt;CODENAME_IN_UPPERCASE&gt;\u003C\u002Fcode> (hyphens in the codename being replaced by underscores). The value of each variable encodes, in order: the database name, host, port, user, and password — separated by commas. A utility function exposes the list of known tenant codenames, used in particular to propagate global secrets to each tenant database.\u003C\u002Fp>\n\n\u003Cp>The \"one tenant = one canonical codename\" convention applies to approximately fifteen surfaces of the system (configuration, routing, database, logs, etc.). The client resolution function is one of the key surfaces.\u003C\u002Fp>\n\n\u003Chr>\n\n\u003Ch2 id=\"adaptateur-postgres\">The PostgreSQL Adapter\u003C\u002Fh2>\n\n\u003Cp>The PostgreSQL adapter exposes a \u003Ccode>query\u003C\u002Fcode> \u002F \u003Ccode>get\u003C\u002Fcode> \u002F \u003Ccode>run\u003C\u002Fcode> interface identical to the legacy MySQL interface, enabling transparent substitution. It relies on the \u003Cstrong>postgres-js\u003C\u002Fstrong> library and performs on-the-fly conversion of legacy MySQL SQL.\u003C\u002Fp>\n\n\u003Ch3>The Connection Pool\u003C\u002Fh3>\n\n\u003Cp>A connection singleton is instantiated with the following parameters (read from environment variables):\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Host, port, user, and database name — with internal default values if variables are absent\u003C\u002Fli>\n  \u003Cli>Database password — \u003Cstrong>required\u003C\u002Fstrong>: the adapter raises an immediate exception if this variable is absent\u003C\u002Fli>\n  \u003Cli>Pool: 20 maximum connections, 60 s idle timeout, 1 800 s maximum lifetime, 15 s connection timeout\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>All queries are executed within the PostgreSQL schema dedicated to the mothership.\u003C\u002Fp>\n\n\u003Ch3>The SQL Translation Layer\u003C\u002Fh3>\n\n\u003Cp>Each SQL query is passed through a rewriting pipeline before execution. Transformations are applied in the following order:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>#\u003C\u002Fth>\n      \u003Cth>Transformation\u003C\u002Fth>\n      \u003Cth>Detail\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>1\u003C\u002Ftd>\n      \u003Ctd>Backticks → double quotes\u003C\u002Ftd>\n      \u003Ctd>Conversion of MySQL identifier delimiters to PostgreSQL syntax\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2\u003C\u002Ftd>\n      \u003Ctd>Schema qualification\u003C\u002Ftd>\n      \u003Ctd>Tables prefixed by internal namespaces are automatically prefixed with the PostgreSQL schema name after the keywords \u003Ccode>FROM\u003C\u002Fcode>, \u003Ccode>JOIN\u003C\u002Fcode>, \u003Ccode>INTO\u003C\u002Fcode>, \u003Ccode>UPDATE\u003C\u002Fcode>, \u003Ccode>TABLE\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2b\u002F2c\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>DATE_SUB\u003C\u002Fcode> \u002F \u003Ccode>DATE_ADD\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Rewritten as PostgreSQL interval arithmetic, for the units DAY, MONTH, YEAR, HOUR, MINUTE, SECOND, on both literals and placeholders\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>2d\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>TIMESTAMPDIFF\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Converted to \u003Ccode>FLOOR(EXTRACT(EPOCH FROM …) \u002F divisor)\u003C\u002Fcode> according to the requested unit\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>3\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>IFNULL\u003C\u002Fcode> → \u003Ccode>COALESCE\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>PostgreSQL does not recognise \u003Ccode>IFNULL\u003C\u002Fcode>\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>4\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>INSERT IGNORE\u003C\u002Fcode> → \u003Ccode>ON CONFLICT DO NOTHING\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>No-op if \u003Ccode>ON CONFLICT\u003C\u002Fcode> is already present\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>5\u003C\u002Ftd>\n      \u003Ctd>Auto-quoting of camelCase aliases\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>AS fooBar\u003C\u002Fcode> → \u003Ccode>AS \"fooBar\"\u003C\u002Fcode> to preserve case (PostgreSQL lowercases unquoted identifiers). Exception: native PostgreSQL types are left as-is to avoid interfering with \u003Ccode>CAST(x AS TEXT)\u003C\u002Fcode> expressions\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>6\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>?\u003C\u002Fcode> placeholders → \u003Ccode>$1, $2, …\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Positional conversion by a character-by-character parser that ignores \u003Ccode>?\u003C\u002Fcode> characters appearing inside quoted strings\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>\u003Cstrong>Parameter safety.\u003C\u002Fstrong> The placeholder transformation only renames markers within the SQL text. The actual binding of values is delegated to postgres-js via its native parameterised query mechanism — no value is interpolated into the SQL string. The parameter array is passed through as-is, without modification. There is therefore no SQL injection risk on bound parameters.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Cases not handled automatically\u003C\u002Fstrong> (the calling code must handle these manually): \u003Ccode>ON DUPLICATE KEY UPDATE\u003C\u002Fcode>, \u003Ccode>LAST_INSERT_ID()\u003C\u002Fcode>, \u003Ccode>GROUP_CONCAT\u003C\u002Fcode>, \u003Ccode>FIND_IN_SET\u003C\u002Fcode>, \u003Ccode>DATE_FORMAT\u003C\u002Fcode>, \u003Ccode>CURDATE()\u003C\u002Fcode>. When an endpoint requires one of these, a dedicated conditional branch is added on the caller side.\u003C\u002Fp>\n\n\u003Ch3>Interface and MySQL \u003Ccode>insertId\u003C\u002Fcode> Emulation\u003C\u002Fh3>\n\n\u003Cp>The adapter interface exposes three methods:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>\u003Ccode>query&lt;T&gt;(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — returns a typed array of results\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>get&lt;T&gt;(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — returns the first result or \u003Ccode>null\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>\u003Ccode>run(sql, params?)\u003C\u002Fcode>\u003C\u002Fstrong> — returns an object \u003Ccode>{ affectedRows, insertId }\u003C\u002Fcode> emulating MySQL behaviour\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The \u003Ccode>run()\u003C\u002Fcode> method emulates the MySQL \u003Ccode>insertId\u003C\u002Fcode>: for an \u003Ccode>INSERT\u003C\u002Fcode> without a \u003Ccode>RETURNING\u003C\u002Fcode> clause or \u003Ccode>ON CONFLICT\u003C\u002Fcode>, it automatically appends \u003Ccode>RETURNING id_&lt;entity&gt;\u003C\u002Fcode> following the primary key naming convention. This logic is disabled for tables with a composite primary key (an explicit list in the source code) as well as for all tables suffixed \u003Ccode>_lang\u003C\u002Fcode> or \u003Ccode>_shop\u003C\u002Fcode>, which do not have a unique \u003Ccode>id_&lt;table&gt;\u003C\u002Fcode> column.\u003C\u002Fp>\n\u003Ch2 id=\"drizzle-schema-as-code\">Drizzle ORM — schema-as-code for table structure\u003C\u002Fh2>\n\n\u003Cp>The paths described in the previous sections (TypeScript access on the Nuxt side, Python access on the tooling side) perform \u003Cstrong>DML\u003C\u002Fstrong>: they read and write rows against a table structure assumed to already be in place. Neither of them creates or alters tables. The \u003Cstrong>structure\u003C\u002Fstrong> (DDL — \u003Ccode>CREATE TABLE\u003C\u002Fcode>, columns, types, indexes, constraints) is governed by a third path: \u003Cstrong>Drizzle ORM\u003C\u002Fstrong>, used in \u003Cem>schema-as-code\u003C\u002Fem> mode. TypeScript schemas are the \u003Cstrong>declared source of truth\u003C\u002Fstrong> for table structure; the \u003Ccode>CREATE\u003C\u002Fcode>\u002F\u003Ccode>ALTER\u003C\u002Fcode> SQL is generated (or hand-written) and then applied to the database, never the other way around.\u003C\u002Fp>\n\n\u003Cp>The dialect was MariaDB before the migration to PostgreSQL, which took effect during an earlier decoupling effort. The relevant dependencies are \u003Ccode>drizzle-orm\u003C\u002Fcode> and \u003Ccode>drizzle-kit\u003C\u002Fcode> at their current versions.\u003C\u002Fp>\n\n\u003Ch3>Drizzle configuration\u003C\u002Fh3>\n\n\u003Cp>The configuration file at the root of the repository defines the following parameters:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Key\u003C\u002Fth>\n      \u003Cth>Value\u003C\u002Fth>\n      \u003Cth>Note\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>dialect\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>'postgresql'\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Replaces the former MariaDB dialect\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>schema\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Three globs covering the OSS core, cockpit modules, and enterprise packs\u003C\u002Ftd>\n      \u003Ctd>Locations of TypeScript declarations\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>out\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Generated SQL migrations folder, inside the OSS core\u003C\u002Ftd>\n      \u003Ctd>Destination for produced \u003Ccode>.sql\u003C\u002Fcode> files\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>schemaFilter\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Dedicated application schema\u003C\u002Ftd>\n      \u003Ctd>Restricts introspection\u002Fdiff to our schema, ignores \u003Ccode>public\u003C\u002Fcode> and native schemas\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>DB credentials\u003C\u002Ftd>\n      \u003Ctd>Carried by environment variables (host, port, user, DB password, database name)\u003C\u002Ftd>\n      \u003Ctd>Never written in plaintext in the repository\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>strict\u003C\u002Fcode> \u002F \u003Ccode>verbose\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>\u003Ccode>true\u003C\u002Fcode> \u002F \u003Ccode>true\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Confirmation before push, verbose output\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ The default port configured here targets the TCP exposure of the database on the host machine (host mapping), not the container's internal port. \u003Ccode>drizzle-kit\u003C\u002Fcode> is designed to point at a TCP-exposed database, not to access the container's internal process directly.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Ch3>drizzle-kit commands\u003C\u002Fh3>\n\n\u003Cp>No dedicated npm scripts are exposed in the project manifest. Commands are run directly via \u003Ccode>npx\u003C\u002Fcode>:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Command\u003C\u002Fth>\n      \u003Cth>Effect\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit generate\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Compares TS schemas against the known state and writes a new SQL migration to the output folder\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit migrate\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Applies pending migrations to the database targeted by the credentials\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>\u003Ccode>npx drizzle-kit introspect\u003C\u002Fcode>\u003C\u002Ftd>\n      \u003Ctd>Reverse-engineers an existing database into TypeScript files (useful for onboarding a legacy table not yet declared)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Ch3>TypeScript schemas\u003C\u002Fh3>\n\n\u003Cp>Each file declares one or more tables via Drizzle's \u003Ccode>pgSchema(...).table(...)\u003C\u002Fcode> API. Schemas are distributed across \u003Cstrong>three scopes\u003C\u002Fstrong> corresponding to the three configuration globs:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Scope\u003C\u002Fth>\n      \u003Cth>Contents\u003C\u002Fth>\n      \u003Cth>Audited by the drift detector\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>OSS core + store tables\u003C\u002Ftd>\n      \u003Ctd>Historical tables of the e-commerce platform (approximately 85 files, including re-exports)\u003C\u002Ftd>\n      \u003Ctd>✅ Yes\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Mother-ship cockpit modules\u003C\u002Ftd>\n      \u003Ctd>Agents, projects, tasks, invoicing, drill… (approximately 47 files)\u003C\u002Ftd>\n      \u003Ctd>❌ No\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Enterprise packs\u003C\u002Ftd>\n      \u003Ctd>Advanced business extensions (approximately 14 files)\u003C\u002Ftd>\n      \u003Ctd>❌ No\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Important note:\u003C\u002Fstrong> a typing reference file defines the module registry table, but it falls outside the three active globs — it serves as an illustration and typing reference, not as an active DDL input for Drizzle. Cockpit tables are declared inside the modules (scope 2); their migrations go through a separate system (see below), not through the Drizzle DDL.\u003C\u002Fp>\n\n\u003Cp>The drift detector only reads the \u003Cstrong>OSS core scope\u003C\u002Fstrong>. Columns declared in cockpit modules or enterprise packs do not trigger a blocking drift — which is why recently added columns can coexist in the database and in the entity layer without being present in the module's Drizzle schema.\u003C\u002Fp>\n\n\u003Cp>What TypeScript schemas type: physical column name, PostgreSQL type, \u003Ccode>notNull\u003C\u002Fcode>, \u003Ccode>default\u003C\u002Fcode>\u002F\u003Ccode>defaultNow\u003C\u002Fcode>, \u003Ccode>primaryKey\u003C\u002Fcode> (simple or composite), \u003Ccode>unique\u003C\u002Fcode>, indexes. Business types are refined via \u003Ccode>$type&lt;...&gt;()\u003C\u002Fcode> (TS typing with no DB constraint). Example declaration for a module registry table:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>export const appSchema = pgSchema('application_schema_name')\n\nexport type Runtime      = 'ps' | 'nuxt'\nexport type ModuleStatus = 'active' | 'disabled' | 'deprecated'\n\nexport const moduleRegistryTable = appSchema.table('registry_table_name', {\n  idModuleRegistry: serial('id_module_registry').primaryKey(),\n  codename: varchar('codename', { length: 128 }).notNull().unique(),\n  version:  varchar('version',  { length: 32 }).notNull(),\n  runtime:  varchar('runtime', { length: 4 }).$type&lt;Runtime&gt;().notNull().default('ps'),\n  status:   varchar('status',  { length: 10 }).$type&lt;ModuleStatus&gt;().notNull().default('active'),\n  manifestJson: text('manifest_json').$type&lt;ModuleManifest | null&gt;(),\n  \u002F\u002F … schema hash, last migration date, dateAdd, dateUpd\n}, (t) =&gt; ({\n  kRuntimeStatus: index('idx_runtime_status').on(t.runtime, t.status)\n}))\n\nexport type RegistryRow    = typeof moduleRegistryTable.$inferSelect\nexport type RegistryInsert = typeof moduleRegistryTable.$inferInsert\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Key takeaways:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>MariaDB ENUMs\u003C\u002Fstrong> have been ported to \u003Ccode>varchar(N) + $type&lt;Union&gt;()\u003C\u002Fcode>: the constraint is enforced at the TypeScript level, not as a native PostgreSQL ENUM type.\u003C\u002Fli>\n  \u003Cli>Multilingual tables with composite primary keys translate to \u003Ccode>primaryKey({ columns: [t.idFaq, t.idLang] })\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>The exported \u003Ccode>$inferSelect\u003C\u002Fcode>\u002F\u003Ccode>$inferInsert\u003C\u002Fcode> types provide row types consumable on the code side. The Nuxt runtime goes through its own adapter (direct DML access), not through the Drizzle query builder — here Drizzle is used solely for DDL and typing.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Generated migrations\u003C\u002Fh3>\n\n\u003Cp>The OSS core migrations folder currently contains three idempotent migrations: addition of logistics columns, import mapping, and addition of a phone column on the customer table for B2B\u002FC registration. The Drizzle journal file (\u003Ccode>meta\u002F_journal.json\u003C\u002Fcode>) shows an \u003Cstrong>empty entry list\u003C\u002Fstrong>.\u003C\u002Fp>\n\n\u003Cblockquote>\n  \u003Cp>⚠️ \u003Cstrong>Structural fact:\u003C\u002Fstrong> the automatic application tracking of \u003Ccode>drizzle-kit migrate\u003C\u002Fcode> is \u003Cem>not\u003C\u002Fem> the operational path. The SQL files in the output folder are hand-written as idempotent (\u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, \u003Ccode>CREATE INDEX IF NOT EXISTS\u003C\u002Fcode>) and applied manually on each customer database. In practice, Drizzle and the TypeScript schemas constitute the \u003Cstrong>declarative\u003C\u002Fstrong> source of truth; actual application remains a manual per-database operation, never auto-propagated.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\n\u003Cp>\u003Cstrong>Second migration path (cockpit tables):\u003C\u002Fstrong> mother-ship cockpit tables — declared inside modules, outside the drift detector's scope — use a separate system of manual SQL migrations, spread across two folders:\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>A folder dedicated to the mother-ship schema (synedre.com, Odyssée, documentation, AI routing…). Files are named by date and subject, all idempotent.\u003C\u002Fli>\n  \u003Cli>A root folder for migrations applied directly to the local database (cockpit tables outside the mother-ship). This folder also hosts archiving sub-folders (\u003Ccode>applied\u002F\u003C\u002Fcode>, \u003Ccode>_applied\u002F\u003C\u002Fcode>) that track migrations already executed on the primary target.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Application is manual in both cases — these migrations do not go through the automatic drift application mechanism described below.\u003C\u002Fp>\n\n\u003Ch3>The TS ↔ live database drift detector\u003C\u002Fh3>\n\n\u003Cp>Since migration application is manual and multi-tenant, an \u003Ccode>ALTER\u003C\u002Fcode> applied on one database but forgotten on another could silently reach production. An audit script (approximately 315 lines) closes this gap.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>How it works:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>The parsing function reads TypeScript files from the OSS core scope and extracts, via regex, the list of tables and their columns.\u003C\u002Fli>\n  \u003Cli>The database query function reads \u003Ccode>information_schema.columns\u003C\u002Fcode> for the relevant application schema, restricted to core-scope tables, against each target database.\u003C\u002Fli>\n  \u003Cli>The per-target diff distinguishes: \u003Cstrong>blocking\u003C\u002Fstrong> = table or column declared in TS but absent from the database (missing migration); \u003Cstrong>info\u003C\u002Fstrong> = present in the database but not in TS.\u003C\u002Fli>\n  \u003Cli>False positives are filtered by an exclusions file: mother-ship-only tables to ignore on customer databases, and OSS store tables removed from the central database during an earlier decoupling effort.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>\u003Cstrong>Exit codes:\u003C\u002Fstrong> \u003Ccode>0\u003C\u002Fcode> — no blocking drift; \u003Ccode>1\u003C\u002Fcode> — blocking drift detected; \u003Ccode>2\u003C\u002Fcode> — execution error.\u003C\u002Fp>\n\n\u003Cp>The audit can be run against a single database (default mode, backward-compatible), a named tenant, all tenants, or with verbose output.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Deployment gate:\u003C\u002Fstrong> the audit is wired as a \u003Cstrong>blocking\u003C\u002Fstrong> step in the deployment pipeline. Before pushing an artifact, the pipeline verifies that the target tenant's database has all the columns expected by the code. If drift is detected, the deployment stops. This is a pre-deployment check; no dedicated periodic cron job exists.\u003C\u002Fp>\n\n\u003Ch3>The automatic DDL drift applicator\u003C\u002Fh3>\n\n\u003Cp>A second script (approximately 553 lines) complements the audit: where the audit \u003Cem>reports\u003C\u002Fem> drift, this script \u003Cem>fixes\u003C\u002Fem> it.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Core principles:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n  \u003Cli>\u003Cstrong>No \u003Ccode>DROP\u003C\u002Fcode> ever\u003C\u002Fstrong> (neither table nor column). Only missing items are added.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Idempotent:\u003C\u002Fstrong> re-runnable with no effect if the database is already up to date.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Single transaction\u003C\u002Fstrong> with halt on error: full rollback if any statement fails.\u003C\u002Fli>\n  \u003Cli>Generates only \u003Ccode>ADD COLUMN IF NOT EXISTS\u003C\u002Fcode> and \u003Ccode>CREATE TABLE IF NOT EXISTS\u003C\u002Fcode>, with types, \u003Ccode>NOT NULL\u003C\u002Fcode>\u002F\u003Ccode>DEFAULT\u003C\u002Fcode> constraints, and checks derived from the TypeScript schemas.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The script reuses the auditor's definitions (target list, schema parsers, exclusion loading) to maintain a single source of truth.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Internal pipeline:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n  \u003Cli>Parsing TypeScript schemas into \u003Ccode>TableDef\u003C\u002Fcode>\u002F\u003Ccode>ColumnDef\u003C\u002Fcode> structures (PG types, nullable, default, check).\u003C\u002Fli>\n  \u003Cli>Computing the TS ↔ live database drift: missing tables → \u003Ccode>CREATE TABLE\u003C\u002Fcode>; missing columns → \u003Ccode>ADD COLUMN\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Generating the idempotent SQL block.\u003C\u002Fli>\n  \u003Cli>Executing in a transaction against the target database (only if the \u003Ccode>--apply\u003C\u002Fcode> flag is passed).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Usage mode\u003C\u002Fth>\n      \u003Cth>Effect\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Dry-run on a tenant\u003C\u002Ftd>\n      \u003Ctd>Displays the SQL without executing it (default behavior)\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Apply on a tenant\u003C\u002Ftd>\n      \u003Ctd>Applies the SQL in a transaction against the target database\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Dry-run on all tenants\u003C\u002Ftd>\n      \u003Ctd>Inspects all databases without making any changes\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>\u003Cstrong>Exit codes:\u003C\u002Fstrong> \u003Ccode>0\u003C\u002Fcode> — no drift (or successful application); \u003Ccode>1\u003C\u002Fcode> — drift detected in dry-run, or error during application; \u003Ccode>2\u003C\u002Fcode> — parsing or connection error.\u003C\u002Fp>\n\n\u003Cp>The applicator is wired into the unified deployment path, conditioned by an activation flag (\u003Ccode>DRIFT_AUTO_APPLY\u003C\u002Fcode>, disabled by default — explicit gate). Since the consolidation of deployment scripts during a recent effort, all tenants go through this unified path; the old per-tenant deployment scripts have been removed, closing the historical gap where some tenants did not benefit from automatic remediation.\u003C\u002Fp>\n\n\u003Ch3>Authoritative source for structural changes\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>TypeScript schema files           ← DECLARATIVE SOURCE OF TRUTH for DDL\n   │  (1) edit the TS\n   ▼\nIdempotent SQL migration\n   │  (2a) Manual application per impacted database\n   │  (2b) Automatic drift applicator (flag DRIFT_AUTO_APPLY=1)\n   ▼\nLive PostgreSQL database (mother-ship + customer databases)\n   ▲\n   └─ (3) Drift detector: verifies TS == live, blocks deployment on mismatch\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Cstrong>Change rule:\u003C\u002Fstrong> to evolve a table in the core scope, first edit the TypeScript schema, generate or hand-write the idempotent migration, apply it on each database (manual or automatic path), then re-run the audit — the deployment pipeline will replay it regardless. Never modify the live database without reflecting the change in the TypeScript schema: the audit would flag it as blocking drift on the next deployment.\u003C\u002Fp>\n\n\u003Cp>Conversely, the Python Entity classes used by the tooling never create columns: their field list is a \u003Cem>whitelist\u003C\u002Fem> of columns assumed to already exist in the database, not a structure declaration.\u003C\u002Fp>\n\u003Ch2 id=\"acces-agentique-pattern-entity\">Agentic Access (Python) — the Entity Pattern\u003C\u002Fh2>\n\n\u003Cp>The mother-ship Python code does not access the database through a network connection pool the way the frontend runtime does. It goes through a \u003Cstrong>subprocess mechanism\u003C\u002Fstrong> that delegates SQL execution to the database container. A shared base class centralises this transport and exposes a generic CRUD layer from which roughly forty specialised business classes inherit.\u003C\u002Fp>\n\n\u003Ch3>Low-level transport\u003C\u002Fh3>\n\n\u003Cp>Three helpers on the base class cover the common use cases:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>Helper\u003C\u002Fth>\n      \u003Cth>Usage\u003C\u002Fth>\n      \u003Cth>Notes\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>Safe write\u003C\u002Ftd>\n      \u003Ctd>INSERT \u002F UPDATE \u002F DELETE\u003C\u002Ftd>\n      \u003Ctd>The SQL is written to a uniquely named temporary file (PID + UUID to avoid multi-process collisions), copied into the container, then executed with immediate halt on error (\u003Ccode>ON_ERROR_STOP=1\u003C\u002Fcode>). No stdin pipe is used.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>Tabular read\u003C\u002Ftd>\n      \u003Ctd>SELECT on simple columns\u003C\u002Ftd>\n      \u003Ctd>SQL passed on the command line, result tab-separated. Should be avoided on TEXT columns containing newlines.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>CSV read\u003C\u002Ftd>\n      \u003Ctd>SELECT on multi-line TEXT columns\u003C\u002Ftd>\n      \u003Ctd>CSV mode (RFC 4180); NULL values are returned as empty strings.\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Every query automatically prefixes the session with the appropriate application schema. Connection parameters (target container, database name, user, DB password, schema) are read from the process environment variables — no credentials are hard-coded.\u003C\u002Fp>\n\n\u003Ch3>The \u003Ccode>Entity\u003C\u002Fcode> class — generic CRUD\u003C\u002Fh3>\n\n\u003Cp>The base class exposes a CRUD layer parameterised by three class attributes:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Table name\u003C\u002Fstrong> — the target physical table.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Primary key\u003C\u002Fstrong> — name of the PK column, used in \u003Ccode>RETURNING\u003C\u002Fcode> and \u003Ccode>WHERE\u003C\u002Fcode> clauses.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Field whitelist\u003C\u002Fstrong> — only columns declared here are accepted in INSERT\u002FUPDATE; any column absent from the list is silently ignored.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Standard behaviour of each method:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Create\u003C\u002Fstrong>: business validation → automatic injection of the canonical \u003Ccode>client_id\u003C\u002Fcode> if the column is in the whitelist → whitelist filtering → \u003Ccode>INSERT … RETURNING &lt;pk&gt;\u003C\u002Fcode> with \u003Ccode>NOW()\u003C\u002Fcode> timestamp on \u003Ccode>date_add\u003C\u002Fcode> and \u003Ccode>date_upd\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Update\u003C\u002Fstrong>: whitelist filtering + update of \u003Ccode>date_upd\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Search \u002F existence check \u002F delete\u003C\u002Fstrong>: standard implementations.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>Internal SQL escaping natively handles booleans (\u003Ccode>TRUE\u003C\u002Fcode>\u002F\u003Ccode>FALSE\u003C\u002Fcode>), dictionaries, and lists (serialised as JSON).\u003C\u002Fp>\n\n\u003Cp>Sub-classes override the \u003Ccode>validate(data, mode)\u003C\u002Fcode> method to place business rules: blocking violations raise a \u003Ccode>ValidationError\u003C\u002Fcode>; non-blocking warnings are collected and returned to the caller without interrupting the operation.\u003C\u002Fp>\n\n\u003Ch2 id=\"creation-atomique-chantier\">Atomic creation of a worksite with its skeleton\u003C\u002Fh2>\n\n\u003Cp>To prevent orphaned worksites (with no job or task), the worksite component's creation method creates the following in a single \u003Ccode>BEGIN … COMMIT\u003C\u002Fcode> transaction:\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>The \u003Cstrong>worksite\u003C\u002Fstrong> itself.\u003C\u002Fli>\n  \u003Cli>A \u003Cstrong>first job\u003C\u002Fstrong> attached to the worksite (lookup by codename).\u003C\u002Fli>\n  \u003Cli>One or more \u003Cstrong>initial tasks\u003C\u002Fstrong> attached to the job (lookup by job identifier).\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>The \u003Ccode>ON_ERROR_STOP=1\u003C\u002Fcode> option guarantees a full rollback if any one of the inserts fails.\u003C\u002Fp>\n\n\u003Ch3>Blocking validations\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>The codename must conform to kebab-case format (4 to 64 characters) and be unique.\u003C\u002Fli>\n  \u003Cli>Priority must belong to the set \u003Ccode>{P0, P1, P2, P3}\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>Each task must have a non-empty title and assignee; the assignee must exist in the agent registry.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Multi-agent staffing\u003C\u002Fstrong>: for any worksite whose scope concerns a client tenant, at least two distinct agents must be assigned — a rule derived from the cross-supervision doctrine.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Automatic behaviours\u003C\u002Fh3>\n\n\u003Cul>\n  \u003Cli>The fields \u003Ccode>priority\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>estimated_tokens\u003C\u002Fcode>, \u003Ccode>estimated_h\u003C\u002Fcode>, \u003Ccode>position\u003C\u002Fcode>, \u003Ccode>recommended_model\u003C\u002Fcode>, \u003Ccode>scope\u003C\u002Fcode>, \u003Ccode>visual_intent\u003C\u002Fcode>, and \u003Ccode>visual_url\u003C\u002Fcode> are transparently propagated from the worksite to each task.\u003C\u002Fli>\n  \u003Cli>If \u003Ccode>recommended_model\u003C\u002Fcode> is absent from a task, it is automatically computed by the model-selection heuristic (see below).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"entite-tache-estimation-modele\">Task Entity — estimation, recommended model, skills and tools\u003C\u002Fh2>\n\n\u003Ch3>Automatic estimation\u003C\u002Fh3>\n\n\u003Cp>When a task is created, if the token volume is not supplied, a dedicated estimator is called automatically. A non-blocking warning is emitted if the estimate remains missing.\u003C\u002Fp>\n\n\u003Ch3>AI model selection heuristic\u003C\u002Fh3>\n\n\u003Cp>The model recommendation method follows a cascade of criteria:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Most capable model\u003C\u002Fstrong> if the priority is P0, or if the task has failed repeatedly (≥ 2 failed iterations), or if the estimated volume exceeds 8,000 tokens.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Mid-tier model\u003C\u002Fstrong> if the volume is between 1,500 and 8,000 tokens.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Lightweight model\u003C\u002Fstrong> for tasks below 1,500 tokens with no aggravating factor.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch3>Attaching skills and tools\u003C\u002Fh3>\n\n\u003Cp>Two methods allow a task to be qualified:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Skill\u003C\u002Fstrong>: lookup in the skill registry by name (natural key) → insertion into the join table with an \u003Ccode>ON CONFLICT … DO NOTHING\u003C\u002Fcode> clause. If the skill is unknown, a proposal is recorded in \u003Cem>pending\u003C\u002Fem> status and the method returns \u003Ccode>False\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Tool\u003C\u002Fstrong>: same logic via the tool registry (lookup by slug). Unknown tool → \u003Cem>pending\u003C\u002Fem> proposal + return \u003Ccode>False\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2 id=\"cascades-de-statut\">Status cascades\u003C\u002Fh2>\n\n\u003Cp>Status updates on tasks and jobs trigger automatic cascades to higher levels:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>Task → done \u002F cancelled\n  (condition: all tasks in the job are in a terminal state,\n              with at least one in done)\n  └─ Cascade to the Job\n       ├─ A QA team is staffed?\n       │    └─ Yes → the QA verdict is applied (validation run)\n       └─ No → Job moves to done\n\nJob → done \u002F cancelled\n  (condition: all jobs in the worksite are terminal,\n              with at least one in done)\n  └─ Cascade to the Worksite\n       ├─ Discovery-only safeguard:\n       │    all jobs completed in discovery phase\n       │    → automatic explosion (an LLM agent generates the implementation jobs)\n       │    instead of promoting the worksite\n       └─ Otherwise → Worksite moves to test status (pre-prod, awaiting review)\n            └─ Non-blocking warning if the pre-prod test plan\n               or the delivery command are missing\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>Automatic resolution of blocked jobs\u003C\u002Fh3>\n\n\u003Cp>A corrective job can be linked to a parent job on hold. When the corrective job moves to \u003Ccode>done\u003C\u002Fcode>, the parent job is automatically closed: its status changes from \u003Ccode>paused\u003C\u002Fcode> to \u003Ccode>done\u003C\u002Fcode>, its remaining tasks move to \u003Ccode>cancelled\u003C\u002Fcode>, and the decision is recorded in the parent job's audit log.\u003C\u002Fp>\n\n\u003Ch2 id=\"conventions-i18n\">Internationalisation conventions and polymorphism\u003C\u002Fh2>\n\n\u003Ch3>Translation tables (\u003Ccode>_lang\u003C\u002Fcode>)\u003C\u002Fh3>\n\n\u003Cp>Any text visible to a visitor lives in a sibling table suffixed \u003Ccode>_lang\u003C\u002Fcode>, never in the parent table.\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>The suffix is exactly \u003Ccode>_lang\u003C\u002Fcode> — not \u003Ccode>_translation\u003C\u002Fcode>, \u003Ccode>_i18n\u003C\u002Fcode>, or \u003Ccode>_locale\u003C\u002Fcode>.\u003C\u002Fli>\n  \u003Cli>The primary key is \u003Cstrong>composite\u003C\u002Fstrong>: \u003Ccode>(id_&lt;entity&gt;, id_lang)\u003C\u002Fcode>, with no auto-increment. In a multi-shop context, the PK becomes \u003Ccode>(id_&lt;entity&gt;, id_lang, id_shop)\u003C\u002Fcode> — no separate \u003Ccode>_shop_lang\u003C\u002Fcode> table is created.\u003C\u002Fli>\n  \u003Cli>Strict separation of concerns: the parent table carries foreign keys, flags, dates, and enumerations; the \u003Ccode>_lang\u003C\u002Fcode> table carries text fields (\u003Ccode>title\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>meta_*\u003C\u002Fcode>, and any visitor-facing text).\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>This convention has a direct impact on the query adapter: \u003Ccode>_lang\u003C\u002Fcode> tables are excluded from the primary-key retrieval heuristic because they do not have a simple PK. On the frontend side, all strings go through the translation function; no UI string may be hard-coded in the source code.\u003C\u002Fp>\n\n\u003Ch3>Polymorphism via \u003Ccode>parent_type\u003C\u002Fcode> \u002F \u003Ccode>parent_id\u003C\u002Fcode>\u003C\u002Fh3>\n\n\u003Cp>When a feature applies to several parent entity types, a \u003Cstrong>single polymorphic table\u003C\u002Fstrong> is created — never one table per parent type. The \u003Ccode>parent_type\u003C\u002Fcode> column carries a textual discriminant (\u003Ccode>'cms'\u003C\u002Fcode>, \u003Ccode>'category'\u003C\u002Fcode>, \u003Ccode>'product'\u003C\u002Fcode>…) and \u003Ccode>parent_id\u003C\u002Fcode> carries the identifier in the corresponding table.\u003C\u002Fp>\n\n\u003Cp>Documented exception: a \u003Cstrong>1:1 extension of a native entity\u003C\u002Fstrong> adopts the extra-table pattern (PK = FK to the native entity), without polymorphism. Pure N:N join tables are named in alphabetical order of the two entities, with no \u003Ccode>_asso\u003C\u002Fcode> or \u003Ccode>_link\u003C\u002Fcode> suffix, and with no associated \u003Ccode>_lang\u003C\u002Fcode> table.\u003C\u002Fp>\n\n\u003Ch3>No business JSON in columns\u003C\u002Fh3>\n\n\u003Cp>JSON content columns (\u003Ccode>payload_json\u003C\u002Fcode>, \u003Ccode>content_i18n\u003C\u002Fcode>, \u003Ccode>labels_json\u003C\u002Fcode>…) are \u003Cstrong>prohibited\u003C\u002Fstrong> for structured business content. The only tolerance applies to \u003Cstrong>ephemeral technical payloads\u003C\u002Fstrong> (webhooks, logs, session state) explicitly documented with a column comment.\u003C\u002Fp>\n\n\u003Cp>In the agentic cockpit, JSON columns are reserved for append-only technical audit trails (decision context, findings, iteration history) — this is the tolerated exception, not the general rule. An automated schema audit (scheduled nightly loop) detects and flags violations at P0 priority.\u003C\u002Fp>\n\n\u003Ch2 id=\"pieges-de-reprise\">Common integration pitfalls\u003C\u002Fh2>\n\n\u003Col>\n  \u003Cli>\n    \u003Cstrong>Shim views vs physical tables\u003C\u002Fstrong>: several entity names exposed in the API are in fact \u003Cem>views\u003C\u002Fem> that read from physical tables under a different name. For reads, the view is sufficient; for writes, the underlying physical table must be targeted. This pattern applies to about a dozen pairs (agents, agent activity, heartbeat, relations, XP, XP history, automata, automaton agents, pipelines, automaton logs, smart-rule logs).\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Container ≠ database ≠ schema\u003C\u002Fstrong>: the database container, the logical database name, and the application schema are three distinct levels. Conflating them produces silent resolution errors.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Non-migrated PostgreSQL tenant\u003C\u002Fstrong>: the query adapter raises an immediate error for any tenant not declared in the list of enabled PostgreSQL tenants. The MySQL fallback path has been removed.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>\u003Ccode>scope\u003C\u002Fcode> enumeration\u003C\u002Fstrong>: the CHECK constraint in the database is the single source of truth for the allowed values of the \u003Ccode>scope\u003C\u002Fcode> field — it may be broader than what the written documentation states.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Rename residue\u003C\u002Fstrong>: the task table has retained its sequences and constraints named after the old prefix, a residue of a partial rename. This is not an error — it is a known state.\n  \u003C\u002Fli>\n  \u003Cli>\n    \u003Cstrong>Legacy MySQL SQL\u003C\u002Fstrong>: an automatic converter transforms MySQL queries to PostgreSQL, but several constructs are not covered (\u003Ccode>GROUP_CONCAT\u003C\u002Fcode>, \u003Ccode>DATE_FORMAT\u003C\u002Fcode>, \u003Ccode>ON DUPLICATE KEY UPDATE\u003C\u002Fcode>…). These cases fail silently and require a manually written native PostgreSQL branch.\n  \u003C\u002Fli>\n\u003C\u002Fol>",{"slug":14,"chapterNum":15,"title":16,"titleEn":17},"overview","01","Vue d'ensemble du harness agentique","Agentic harness overview",{"slug":19,"chapterNum":20,"title":21,"titleEn":22},"agentic-core","03","Le cœur agentique : Atlas et les agents","The Agentic Core: Atlas and the Agents",{"chapters":24},[25,28,29,32,39,46,53,60,67,74,81],{"slug":14,"chapterNum":15,"title":16,"titleEn":17,"summary":26,"summaryEn":27},"Le harness transforme une demande (courriel, console, cron) en action déployée en enchaînant classification, spawn LLM via pseudo-TTY, validation qualité à deux niveaux et boucle d'apprentissage, le tout sans état métier hors base de données.","The harness transforms a request (email, console, cron) into a deployed action by chaining intent classification, pseudo-TTY LLM spawn, two-level quality validation, and a learning loop — with no business state stored outside the database.",{"slug":5,"chapterNum":6,"title":7,"titleEn":8,"summary":9,"summaryEn":10},{"slug":19,"chapterNum":20,"title":21,"titleEn":22,"summary":30,"summaryEn":31},"Décrit l'architecture du moteur agentique : classification LLM des emails entrants, spawn headless de Claude Code via pseudo-TTY, injection des personas agents et orchestration post-spawn (deploy, QA, récap).","Describes the architecture of the agentic engine: LLM classification of incoming emails, headless spawning of Claude Code via pseudo-TTY, injection of agent personas, and post-spawn orchestration (deploy, QA, recap).",{"slug":33,"chapterNum":34,"title":35,"titleEn":36,"summary":37,"summaryEn":38},"chantiers","04","Chantiers, travaux & tâches — modèle de données et API","Worksites, Jobs & Tasks — Data Model and API","Décrit la hiérarchie chantier\u002Ftravail\u002Ftâche de Synedre OS, le modèle de données DB, les statuts\u002Fscopes canoniques et l'API Python associée.","Describes the worksite\u002Fjob\u002Ftask hierarchy in Synedre OS, the DB data model, canonical statuses\u002Fscopes, and the associated Python API.",{"slug":40,"chapterNum":41,"title":42,"titleEn":43,"summary":44,"summaryEn":45},"automates","05","Automates, crons & runs","Automations, crons & runs","Décrit la couche d'exécution non-conversationnelle de Synedre OS : les 209 façades Python, leur enrobage cron, le registre un composant interne et la frontière entre doctrine run et unité agent.","Describes the non-conversational execution layer of Synedre OS: the 209 Python facades, their cron wrapper, the un composant interne registry, and the boundary between run doctrine and agent unit.",{"slug":47,"chapterNum":48,"title":49,"titleEn":50,"summary":51,"summaryEn":52},"hub","06","Le Hub (\u002Fhub\u002F*)","The Hub (\u002Fhub\u002F*)","Présentation de l'interface d'administration privée Synedre OS (mothership-app), son architecture en couches Nuxt, la carte de ses modules et pages, ainsi que le système d'authentification et de session.","Presentation of the Synedre OS private administration interface (mothership-app), its Nuxt layered architecture, the map of its modules and pages, as well as the authentication and session management system.",{"slug":54,"chapterNum":55,"title":56,"titleEn":57,"summary":58,"summaryEn":59},"email","07","Inbox hub & Atlas Inbox — deux pipelines email","Inbox hub & Atlas Inbox — two email pipelines","Décrit les deux pipelines IMAP→DB du harness (contact@ trié pour le hub, atlas@ déclencheur d'actions agentiques), la façade d'envoi sortant et les doctrines bloquantes associées (scan AV, zéro contact client direct).","Describes the two IMAP→DB pipelines of the harness (contact@ sorted for the hub, atlas@ triggering agentic actions), the outbound sending facade, and the associated blocking doctrines (AV scan, zero direct client contact).",{"slug":61,"chapterNum":62,"title":63,"titleEn":64,"summary":65,"summaryEn":66},"memory","08","Mémoire & apprentissage — architecture à trois niveaux","Memory & Learning — Three-Level Architecture","Ce chapitre décrit les trois niveaux de mémoire du harness Synedre OS (réflexe, Zettelkasten, vectoriel pgvector), les automates d'indexation associés et la boucle de capitalisation des erreurs en règles réutilisables.","This chapter describes the three memory levels of the Synedre OS harness (reflex, Zettelkasten, vector pgvector), the associated indexing automata, and the loop for capitalizing errors into reusable rules.",{"slug":68,"chapterNum":69,"title":70,"titleEn":71,"summary":72,"summaryEn":73},"deploy","09","Déploiement & infrastructure","Deployment & infrastructure","Décrit le pipeline de mise en ligne Synedre OS : asymétrie entre .\u002Fdeploy (IA, preprod) et .\u002Fship (Alex, production), le dispatcher YAML, le pattern build-host sans build VPS, et les règles associées (secrets, commit-avant-deploy, drift, smoke).","Describes the Synedre OS release pipeline: asymmetry between .\u002Fdeploy (AI, preprod) and .\u002Fship (Alex, production), the YAML dispatcher, the build-host pattern without build VPS, and the associated rules (secrets, commit-before-deploy, drift, smoke).",{"slug":75,"chapterNum":76,"title":77,"titleEn":78,"summary":79,"summaryEn":80},"facades","10","catalogue des façades et points d'entrée du harness","harness facade and entry-point catalogue","comment le harness synedre organise ses centaines d'automates en familles fonctionnelles, avec les garde-fous en temps réel qui encadrent chaque action.","how the synedre harness organizes its hundreds of automations into functional families, with the real-time guard-rails that frame every action.",{"slug":82,"chapterNum":83,"title":84,"titleEn":85,"summary":86,"summaryEn":87},"skills-hooks","11","Skills, agents & hooks — harness agentique Synedre OS","Skills, agents & hooks — Synedre OS agentic harness","Décrit le câblage complet du harness Claude Code de synedre-os : skills disponibles, sous-agents délégables et hooks d'événements qui imposent la doctrine commit-en-flux, garde-fous prod\u002Femail et injection mémoire.","Describes the complete wiring of the synedre-os Claude Code harness: available skills, delegatable sub-agents, and event hooks that enforce the commit-in-flow doctrine, prod\u002Femail guardrails, and memory injection."]