Structure des opt-ins dans un projet Forge¶
Renommage CLI (ADR-016)
La famille de commandes est désormais forge opt-in:install/remove/enable/disable/list (avec tiret).
Les mentions optin: ci-dessous conservent le nom d'époque des tickets livrés ; la commande actuelle est forge opt-in:enable.
Voir le glossaire opt-in et ADR-016.
Registre unifié (ADR-061)
Depuis ADR-061, optins/registry.py est livré par le squelette (toujours présent) et devient la vue unique des opt-ins du projet : le backend base de données choisi (BACKEND) et tous les opt-ins utilisés (ENABLED_OPTINS, nom vers catégorie), pas seulement les opt-ins route.
forge opt-in:enable/disable y inscrivent et retirent les entrées, forge opt-in:list le lit pour afficher l'état, et forge doctor signale les divergences avec les paquets installés.
Ticket :
OPTINS-PROJECT-STRUCTURE-001.
Ce document pose le contrat d'une convention de branchement local des opt-ins dans une application Forge générée.
Il est architecture + documentation uniquement : aucun code n'est généré, aucune commande n'est ajoutée, aucun paquet n'est déplacé.
L'implémentation viendra dans des tickets ultérieurs.
Objectif¶
Donner à un projet Forge un lieu unique, explicite et lisible pour voir et brancher les modules opt-in qu'il active :
- quels opt-ins sont activés ;
- quelles routes ils ajoutent ;
- quelles migrations ils utilisent ;
- quels starters leur sont liés ;
- quelle documentation locale existe ;
- comment ils sont branchés dans
mvc/routes/__init__.py.
C'est l'équivalent Forge, volontairement simple et sans magie, de ce que les bundles apportent dans Symfony, mais aligné sur la charte Forge (pas d'écriture invisible, refus de la magie cachée, une seule façon officielle de faire).
Différence entre package opt-in et branchement projet¶
Deux choses distinctes, à ne jamais confondre :
| Package opt-in | Branchement projet | |
|---|---|---|
| Où | packages/forge-mvc-* (mono-dépôt Forge) + PyPI |
dossier optins/ dans le projet utilisateur |
| Contient | le code complet du module (logique, API publique) | uniquement le câblage local : routes, migrations utilisées, README, docs locales |
| Installé par | pip install forge-mvc-<module> |
présent dans le projet généré |
| Maintenu par | l'équipe Forge | l'utilisateur (son application) |
Règle verrouillée : les packages distribués restent dans packages/forge-mvc-*.
Le dossier optins/ côté projet ne contient pas le code complet du package, il ne fait que le brancher.
Pourquoi pas de découverte automatique¶
Forge ne fait pas de discovery magique des opt-ins.
Aucune inspection automatique de site-packages, aucun chargement implicite au démarrage, aucun « plugin scan ».
Raisons (charte v2) :
- Refuser la magie cachée (§3) : un opt-in actif doit être visible dans le code du projet, pas deviné à l'exécution.
- Pas d'écriture invisible (§9) : Forge ne réécrit pas silencieusement
mvc/routes/__init__.py. - Une seule façon officielle (§11) : le branchement passe toujours par
optins/registry.py, appelé explicitement.
Conséquence directe : Forge Core ne dépend pas des opt-ins et ne les charge pas automatiquement.
Un opt-in absent ne casse jamais le core.
Dossier optins/¶
Structure cible côté projet utilisateur :
optins/
├── __init__.py
├── registry.py
├── iot/
│ ├── __init__.py
│ ├── routes.py
│ ├── README.md
│ ├── migrations/
│ └── docs/
├── video/
│ ├── __init__.py
│ ├── routes.py
│ └── README.md
└── audio/
├── __init__.py
├── routes.py
└── README.md
Sous-dossier optins/<name>/ : seulement pour les opt-ins route
Seuls les opt-ins de type route (iot, video, audio), qui exposent leurs propres routes HTTP, reçoivent un sous-dossier de câblage optins/<name>/.
Les opt-ins library et crosscutting (mfa, rbac, workflow…) s'utilisent par import direct ou par décorateurs et n'ont pas de sous-dossier.
En revanche, depuis ADR-061, tous les opt-ins utilisés figurent dans ENABLED_OPTINS de optins/registry.py, quel que soit leur kind.
Chaque sous-dossier optins/<module>/ est le point de branchement local d'un package opt-in installé.
Il reste mince : il référence le package, il ne le duplique pas.
optins/registry.py¶
Le registre central est la vue unique des opt-ins du projet (ADR-061), livrée par le squelette et toujours présente.
Il porte trois choses :
BACKEND: le backend base de données choisi (ADR-054/060), ouNone;ENABLED_OPTINS: tous les opt-ins utilisés, par nom vers catégorie (route,library,crosscutting,cli) ;register_optins(router): câble les routes des opt-insrouteactivés.
"""Registre des opt-ins de ce projet (ADR-061)."""
BACKEND: str | None = "sqlite"
ENABLED_OPTINS: dict[str, str] = {
"qrcode": "library",
"iot": "route",
}
def register_optins(router) -> None:
from optins.iot.routes import register as register_iot
register_iot(router)
Le projet appelle register_optins explicitement dans mvc/routes/__init__.py (toujours présent, sans effet tant qu'aucun opt-in route n'est activé) :
Pas de décorateur magique, pas d'auto-import ni de scan du .venv : on lit dans registry.py la liste exacte des opt-ins du projet.
Les entrées sont gérées par forge opt-in:enable/disable (pour tous les kind) ; le backend est inscrit par forge db:init.
Le code des opt-ins, lui, reste dans le .venv.
optins/<module>/routes.py¶
Chaque opt-in expose une fonction register(router) qui délègue à l'API publique du package :
# optins/iot/routes.py
from forge_mvc_iot import register_iot_routes
def register(router):
register_iot_routes(router)
Le code métier vit dans le package (forge_mvc_iot) ; optins/iot/ fait seulement le pont.
C'est l'API publique du package qui reste le contrat de complétude (charte v2 §10).
Migrations opt-in¶
Un opt-in peut apporter des migrations SQL (ex. iot_events pour Forge IoT).
La convention :
- la migration source est packagée dans le module (
forge_mvc_iot/migrations/, package data) ; optins/<module>/migrations/côté projet reçoit la copie locale effectivement appliquée (via l'outil dédié du module, ex.forge iot:init, puisforge migration:apply) ;- le SQL reste visible (charte v2 §5) et appliqué explicitement.
Ce ticket ne déplace ni ne copie aucune migration : il fixe seulement où elle se branchera côté projet.
Starters opt-in¶
Les starters sont des parcours d'apprentissage réalisés à la main depuis la documentation (ADR-035), ils ne sont pas générés.
Chaque progression welcome-<module> montre, palier par palier, le contrôleur, la vue et la route à créer dans le projet.
Le dossier optins/<module>/ reste la cible où l'opt-in se branche côté projet, mais c'est l'apprenant qui crée les fichiers en suivant le parcours, pas une commande de génération.
Documentation locale¶
La documentation complète d'un opt-in reste dans la doc officielle Forge (par exemple les pages docs/iot/*).
Côté projet, chaque optins/<module>/README.md ne reçoit qu'un README utile et minimal : ce que l'opt-in branche dans ce projet, les commandes à lancer, et un lien vers la doc officielle.
On ne duplique pas la documentation de référence dans chaque projet.
Exemple avec Forge IoT¶
Activer Forge IoT dans un projet, avec la convention cible :
optins/
├── registry.py # appelle register_iot(router)
└── iot/
├── routes.py # register(router) -> register_iot_routes(router)
├── migrations/ # copie locale de *_create_iot_events.sql
├── README.md # « IoT branché ici ; voir docs/iot/ »
└── docs/ # notes locales minimales
# optins/iot/routes.py
from forge_mvc_iot import register_iot_routes
def register(router):
register_iot_routes(router) # /api/iot/events, etc.
Le package forge-mvc-iot (dans packages/forge-mvc-iot/) fournit tout le reste : contrat MQTT, subscriber, repository, API HTTP, CLI (forge iot:doctor, iot:init, iot:listen, iot:simulate).
Voir l'architecture Forge IoT et l'audit de clôture IoT.
Exemple vivant : le starter welcome-iot
Cette convention n'est pas que théorique : le starter welcome-iot génère réellement cette structure optins/iot/ dans le projet créé (OPTINS-IOT-PROJECT-BRIDGE-001).
C'est le premier opt-in officiel branché via optins/registry.py.
Les opt-ins route video et audio suivent désormais le même modèle (forge opt-in:enable video|audio).
Les opt-ins library (workflow, stats, images, files, mail, pivot, i18n) et crosscutting (mfa, rbac) ne reçoivent pas cette couche : ils s'utilisent par import direct ou par décorateurs.
Comparaison avec les bundles Symfony¶
| Symfony | Forge (cible optins/) |
|---|---|
| Bundle = package réutilisable | Package opt-in forge-mvc-* |
config/bundles.php liste les bundles actifs |
optins/registry.py liste les opt-ins branchés |
| Auto-configuration / compiler passes | Aucune auto-config, branchement explicite |
| Recipes Flex modifient le projet | Forge n'écrit jamais en invisible (§9) |
| Bundle découvert par le framework | Pas de discovery magique ; appel explicite |
L'esprit est le même (« un endroit pour voir les modules activés »), mais Forge reste plus explicite : tout le branchement est du Python lisible que l'utilisateur contrôle, sans couche d'auto-magie.
Décisions verrouillées¶
- Les packages distribués restent dans
packages/forge-mvc-*. - Le dossier
optins/côté projet ne contient pas le code complet du package. optins/sert au branchement local : routes, migrations, README, docs locales.- Le branchement est explicite via
optins/registry.py. - Forge Core ne charge pas automatiquement tous les opt-ins.
- Pas de discovery magique.
- Les starters opt-in restent gérés par Forge CLI, mais peuvent générer une structure
optins/. - La documentation complète reste dans la doc officielle ; le projet local reçoit seulement un README utile.
Hors périmètre¶
Ce ticket pose le contrat.
Ne sont pas faits ici :
- pas de commande
forge optin:enable/forge optin:disable; - pas de génération automatique de
optins/; - pas de modification de
forge new; - pas de déplacement des packages ;
- aucune modification fonctionnelle IoT / RBAC / media / workflow / stats / MFA ;
- pas de refonte des starters existants ;
- pas de migration automatique.
Tickets suivants¶
OPTINS-IOT-PROJECT-BRIDGE-001(livré), applique concrètement cette structure à Forge IoT : le starterwelcome-optin-iotgénèreoptins/iot/et branche l'API viaoptins/registry.py.OPTINS-CLI-ENABLE-AUDIT-001(livré), cadre la future commandeforge optin:enable: voir l'auditforge optin:enable(commande cible, idempotence, dry-run, gestion des conflits, sans discovery ni écrasement silencieux).OPTINS-CLI-ENABLE-IOT-001(livré), implémenteforge optin:enable iot(dry-run par défaut,--apply, idempotent).
Voir la référence CLI.OPTINS-CLI-ENABLE-ROUTES-APPLY-001(livré),--applybranchemvc/routes/__init__.pysi la structure est reconnue (router = Router()), sinon[WARN]+ instruction manuelle (aucune modification).OPTINS-CLI-LIST-001(livré),forge optin:list, commande lecture seule qui affiche l'état local des opt-insroute(absent/partiel/activépouriot,video,audio) et liste les opt-inslibrary/crosscuttingavec leur kind, sans rien créer ni modifier.