Audit forge optin:enable¶
Renommage CLI (ADR-016)
La commande s'appelle désormais forge opt-in:enable (avec tiret), au sein de la famille forge opt-in:install/remove/enable/disable/list.
Ce document de conception conserve le nom d'époque optin:enable.
Voir le glossaire opt-in et ADR-016.
Ticket :
OPTINS-CLI-ENABLE-AUDIT-001.
Audit de conception, ce document cadre la future commandeforge optin:enablesans l'implémenter.
Aucun code fonctionnel n'est ajouté : pas de modification deforge.py,cli/, du starterwelcome-optin-iot, ni du paquetforge-mvc-iot.
L'implémentation fera l'objet du ticketOPTINS-CLI-ENABLE-IOT-001, après validation de ce contrat.
Implémenté pour iot (OPTINS-CLI-ENABLE-IOT-001 + OPTINS-CLI-ENABLE-ROUTES-APPLY-001)
Ce contrat est désormais réalisé pour le premier opt-in : forge optin:enable iot existe (dry-run par défaut, --apply pour écrire, idempotent).
Le branchement de mvc/routes/__init__.py est implémenté par OPTINS-CLI-ENABLE-ROUTES-APPLY-001 : insertion uniquement si la structure est reconnue (router = Router()), sans marqueurs (l'idempotence repose sur la présence de l'appel register_optins) ; structure ambiguë ou fichier absent → [WARN] + instruction manuelle, aucune écriture.
Ce document reste la référence de conception ; voir la référence CLI.
forge opt-in:enable couvre désormais les trois opt-ins de type route (iot, video, audio), seuls à posséder leurs propres routes et donc à recevoir la couche optins/.
Les opt-ins library (workflow, stats, images, files, mail, pivot, i18n) et crosscutting (mfa, rbac) ne se branchent pas par cette couche : ils s'utilisent par import direct ou par décorateurs, sans câblage optins/.
Objectif¶
Définir précisément ce qu'une commande forge optin:enable <name> aura le droit de faire pour brancher localement un opt-in dans un projet Forge, en respectant la convention figée par la structure des opt-ins :
- quels fichiers elle peut créer ;
- quels fichiers elle peut modifier (et avec quelle prudence) ;
- comment elle reste idempotente ;
- comment elle gère les conflits ;
- comment elle reste explicite, lisible, réversible, pédagogique et sans découverte automatique.
Pourquoi ne pas coder directement¶
forge optin:enable est une commande sensible : elle touche potentiellement optins/, mvc/routes/__init__.py, mvc/migrations/, un README local et des fichiers de configuration.
Coder trop vite risquerait d'introduire :
- de la magie cachée (charte v2 §3), un branchement deviné plutôt qu'écrit ;
- une écriture invisible dans le code utilisateur (charte v2 §9), une modification non maîtrisée de
mvc/routes/__init__.py; - des modifications difficiles à relire ou à annuler.
D'où cet audit préalable : on verrouille le contrat avant d'écrire la moindre ligne.
Commande cible¶
- Premier opt-in supporté :
iot(forge optin:enable iot), parce qu'il a déjà routes HTTP, migration packagée, CLI, starter et documentation.
Les autres modules (rbac,media,workflow,stats,mfa) viendront plus tard, un par un, jamais en bloc. - Options futures envisagées (non décidées définitivement ici, mais cadrées) :
| Option | Effet envisagé |
|---|---|
--dry-run |
n'écrit rien ; affiche ce qui serait créé / modifié (patch, instructions). Doit exister dès la v1. |
--apply |
autorise la modification réelle de mvc/routes/__init__.py (sinon : instruction manuelle affichée). |
--no-migrations |
ne touche pas à mvc/migrations/ (l'utilisateur fera forge iot:init lui-même). |
--no-routes |
crée la couche optins/ mais ne propose pas de brancher mvc/routes/__init__.py. |
Pas de forge optin:disable dans cette trajectoire (hors périmètre, à auditer séparément si besoin).
Modèle de branchement¶
La commande n'invente rien : elle matérialise le modèle déjà documenté, identique à celui généré par le starter welcome-optin-iot.
# optins/registry.py
def register_optins(router):
from optins.iot.routes import register as register_iot
register_iot(router)
# optins/iot/routes.py
from forge_mvc_iot import register_iot_routes
def register(router):
register_iot_routes(router)
Règles verrouillées sur le branchement :
- pas de scan automatique de dossiers (
optins/*n'est pas « activé parce qu'il existe ») ; - pas d'
importlib/pkgutilpour charger tout ce qui traîne ; - chaque opt-in actif est importé et appelé explicitement dans
optins/registry.py; - Forge Core ne dépend pas des opt-ins et n'en charge aucun tout seul.
Fichiers créés¶
Pour forge optin:enable iot, la commande pourra créer (write-if-new) la couche locale, si absente :
optins/
├── __init__.py
├── registry.py # créé s'il n'existe pas
└── iot/
├── __init__.py
├── routes.py
├── README.md
└── migrations/
└── README.md
Ces fichiers sont du câblage local ; ils ne dupliquent pas le code du paquet forge-mvc-iot.
Le README local reste court et renvoie vers la documentation officielle.
Fichiers modifiés¶
Deux fichiers existants peuvent devoir évoluer, traités avec prudence maximale :
optins/registry.py¶
S'il existe déjà, la commande doit ajouter l'appel de l'opt-in (register_iot) sans réécrire le reste du fichier (qui peut contenir d'autres opt-ins ou du code manuel).
Si la forme du registre n'est pas reconnue de façon non ambiguë → WARN + instruction manuelle, pas de réécriture.
mvc/routes/__init__.py¶
C'est le point le plus sensible (code utilisateur, charte v2 §9).
Politique retenue et implémentée (OPTINS-CLI-ENABLE-ROUTES-APPLY-001) :
- en dry-run : la commande n'écrit pas dans
mvc/routes/__init__.py, elle annonce le branchement (serait branché) ; - avec
--apply, si la structure est reconnue (présence derouter = Router(), même heuristique quemake:public-page) : elle insère l'importfrom optins.registry import register_optins(près des imports) et l'appelregister_optins(router)(en fin de fichier), idempotemment ; - sans marqueurs : l'idempotence repose sur la présence de l'appel
register_optins(router)(un 2e--applyne duplique rien).
Choix délibéré pour ne pas ajouter de bruit# forge-optin: …dans le fichier utilisateur ; - si
mvc/routes/__init__.pya une structure non reconnue (ou est absent) → WARN + instruction manuelle, jamais d'insertion « au jugé ».
mvc/migrations/¶
La commande ne crée pas de migration elle-même : elle réutilise le flux existant forge iot:init (copie la migration packagée) puis forge migration:apply.
--no-migrations permet de sauter cette proposition.
Le SQL reste visible et appliqué explicitement (charte v2 §5).
Idempotence¶
La commande doit pouvoir être relancée sans casser le projet :
Deuxième appel attendu (aucun doublon) :
Règles :
- aucun doublon dans
optins/registry.py(l'appelregister_iotn'est ajouté qu'une fois) ; - aucun doublon dans
mvc/routes/__init__.py(l'import et l'appelregister_optins(router)ne sont insérés qu'une fois ; si l'appel est déjà là →[OK] déjà branché) ; - un fichier déjà présent et identique →
[OK]silencieux, exit 0 ; - un fichier présent mais différent → voir conflits.
Gestion des conflits¶
Règle générale : ne jamais écraser silencieusement un fichier utilisateur.
En cas d'ambiguïté → WARN + instructions manuelles, exit non bloquant.
| Situation | Comportement attendu |
|---|---|
optins/iot/routes.py existe et diffère |
[WARN] : ne pas écraser ; afficher le contenu attendu / un diff, laisser l'utilisateur décider |
optins/registry.py existe avec du code manuel |
[WARN] : ne pas réécrire ; afficher la ligne à ajouter |
mvc/routes/__init__.py a une structure non reconnue |
[WARN] : afficher l'instruction de branchement manuelle |
la migration existe déjà dans mvc/migrations/ |
[OK] idempotent (déléguer à forge iot:init, déjà idempotent) |
forge-mvc-iot n'est pas installé |
[ERREUR] claire : pip install forge-mvc-iot, ne rien écrire qui dépende du paquet |
Mode dry-run¶
--dry-run est obligatoire dès la première version :
- n'écrit aucun fichier, ne modifie rien ;
- affiche la liste de ce qui serait créé, le patch qui serait appliqué à
mvc/routes/__init__.py, et les commandes migrations suggérées ; - exit 0.
C'est le mode de revue : on voit l'effet complet avant de l'appliquer.
Cohérent avec l'esprit forge iot:init (copie idempotente, sans exécuter le SQL) et forge update --dry-run.
Cas Forge IoT¶
forge optin:enable iot produira exactement la structure que le starter welcome-iot génère déjà (OPTINS-IOT-PROJECT-BRIDGE-001) :
- crée
optins/+optins/iot/(write-if-new) ; - ajoute
register_iotdansoptins/registry.py(idempotent) ; - propose (ou applique avec
--apply) le branchementregister_optins(router)dansmvc/routes/__init__.py; - rappelle
forge iot:init+forge migration:applypour la tableiot_events; - renvoie vers la doc IoT officielle.
La commande et le starter doivent rester cohérents : même structure, même branchement explicite, mêmes fichiers.
La commande est l'équivalent « sur un projet existant » de ce que le starter fait « à la création ».
Hors périmètre¶
Ce ticket ne fait que cadrer.
Ne sont pas réalisés ici :
- pas de
forge optin:enablefonctionnel ; - pas de
forge optin:disable; - pas de modification de
forge.pyni decli/; - pas de génération automatique de fichiers ;
- pas de modification de
mvc/routes/__init__.py; - pas de modification du starter
welcome-optin-iot; - pas de modification du paquet
forge-mvc-iot; - pas de support
rbac/media/workflow/stats/mfa.
Décision¶
Trajectoire validée pour conception (implémentation au ticket suivant) avec les règles verrouillées :
- la commande cible sera
forge optin:enable <name>; - le premier opt-in supporté sera
iot; - la commande devra être idempotente ;
- le
--dry-rundevra exister dès la première version ; - aucune découverte automatique ne sera introduite ;
- aucun fichier utilisateur ne sera écrasé silencieusement ;
- le branchement restera explicite via
optins/registry.py; - Forge Core ne dépendra toujours pas des opt-ins.
Ce ticket n'implémente pas la commande : il ne crée aucun code fonctionnel, il fixe seulement le contrat.
Prochain ticket : OPTINS-CLI-ENABLE-IOT-001, implémentation réelle de forge optin:enable iot, conforme à ce contrat.