Aller au contenu

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 commande forge optin:enable sans
l'implémenter. Aucun code fonctionnel n'est ajouté : pas de
modification de forge.py, cli/, du starter welcome-optin-iot, ni
du paquet forge-mvc-iot. L'implémentation fera l'objet du ticket
OPTINS-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.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.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.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

forge optin:enable <name>
  • 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.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.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.

# mvc/routes.py
from optins.registry import register_optins

register_optins(router)
# 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 / pkgutil pour 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.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.py,
    elle annonce le branchement (serait branché) ;
  • avec --apply, si la structure est reconnue (présence de
    router = Router(), même heuristique que make:public-page) : elle
    insère l'import from optins.registry import register_optins (près des
    imports) et l'appel register_optins(router) (en fin de fichier),
    idempotemment ;
  • sans marqueurs : l'idempotence repose sur la présence de l'appel
    register_optins(router) (un 2e --apply ne duplique rien). Choix
    délibéré pour ne pas ajouter de bruit # forge-optin: … dans le
    fichier utilisateur ;
  • si mvc/routes.py a 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 :

forge optin:enable iot
forge optin:enable iot

Deuxième appel attendu (aucun doublon) :

[OK] opt-in iot déjà présent
[OK] registre déjà configuré
[OK] routes déjà branchées

Règles :

  • aucun doublon dans optins/registry.py (l'appel register_iot
    n'est ajouté qu'une fois) ;
  • aucun doublon dans mvc/routes.py (l'import et l'appel
    register_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.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.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) :

  1. crée optins/ + optins/iot/ (write-if-new) ;
  2. ajoute register_iot dans optins/registry.py (idempotent) ;
  3. propose (ou applique avec --apply) le branchement
    register_optins(router) dans mvc/routes.py ;
  4. rappelle forge iot:init + forge migration:apply pour la table
    iot_events ;
  5. 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:enable fonctionnel ;
  • pas de forge optin:disable ;
  • pas de modification de forge.py ni de cli/ ;
  • pas de génération automatique de fichiers ;
  • pas de modification de mvc/routes.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 :

  1. la commande cible sera forge optin:enable <name> ;
  2. le premier opt-in supporté sera iot ;
  3. la commande devra être idempotente ;
  4. le --dry-run devra exister dès la première version ;
  5. aucune découverte automatique ne sera introduite ;
  6. aucun fichier utilisateur ne sera écrasé silencieusement ;
  7. le branchement restera explicite via optins/registry.py ;
  8. 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.