ADR-061 : Registre d'opt-ins visible et unifié dans le projet¶
Statut¶
Acceptée, Forge 1.0.0-rc.x (ticket OPTIN-PROJECT-REGISTRY-001).
Implémentée : le registre optins/registry.py est livré par le squelette (toujours présent), forge opt-in:enable/disable inscrivent tous les kind, forge opt-in:list lit le registre, forge db:init inscrit le backend, forge doctor signale les divergences registre vs .venv. Décision d'emplacement retenue : registre dans le squelette (toujours présent). S'appuie sur l'existant (forge opt-in:enable) et se lie à ADR-060 (le backend BDD devient un opt-in choisi par l'utilisateur).
Date¶
2026-07-04
Contexte¶
Les opt-ins Forge sont des paquets PyPI installés dans le .venv du projet (site-packages), jamais copiés dans l'arborescence utilisateur (principe 9).
Leur code est donc invisible dans le projet, ce qui est le bon choix : c'est exactement ce que fait Composer avec vendor/ chez Symfony, et cela préserve la gestion des versions par pip.
Le problème est la découvrabilité : un développeur ne voit pas d'un coup d'œil quels opt-ins son projet utilise.
L'état actuel est partiel et hétérogène selon le type d'opt-in (kind du catalogue cli/optins/catalog.py) :
- les opt-ins
route(iot, video, audio) laissent une trace visible : un dossieroptins/<name>/et une inscription dansoptins/registry.py, posés parforge opt-in:enable; - les opt-ins
library,crosscuttingetcli(rbac, i18n, qrcode, mfa, mail, deploy, etc.) ne laissent aucune trace dans le projet : ils vivent seulement dans le.venv; - le backend BDD (ADR-054, ADR-060) est encore plus invisible, puisqu'il devient une brique installée par l'utilisateur sans marqueur projet.
Les sources de vérité existantes sont insuffisantes prises isolément :
requirements.txtliste ce qui est installé par pip, mêlé aux dépendances non opt-in ; il ne dit pas le rôle ni l'état ;forge opt-in:listlit un catalogue statique et n'affiche l'état projet que pour les opt-insroute; il refuse volontairement de scanner les paquets installés (« pas de discovery magique ») ;pip listmontre l'installé brut, sans lien avec la sémantique Forge.
Le modèle de référence est Symfony Flex, souvent mal résumé.
Symfony ne copie pas le code des bundles dans le projet : il reste dans vendor/.
La lisibilité vient de deux fichiers déclaratifs que Flex écrit dans le projet : composer.json (le manifeste installé) et surtout config/bundles.php, un registre listant une ligne par bundle activé.
Forge a déjà l'équivalent du premier (requirements.txt) et un embryon du second (optins/registry.py), mais ce dernier ne couvre que les opt-ins route.
Décision¶
Le projet porte un registre d'opt-ins unique et visible, optins/registry.py, qui liste tout opt-in utilisé par le projet, quel que soit son kind, plus le backend BDD choisi.
C'est l'équivalent Forge de config/bundles.php : un fichier déclaratif, lisible, qui répond d'un coup d'œil à « quels opt-ins ce projet utilise-t-il ? », sans scanner le .venv.
Le code des opt-ins reste dans le .venv (aucune copie dans le projet, principe 9 préservé).
Registre = opt-ins utilisés, déclaré explicitement¶
Le registre liste les opt-ins que le projet utilise et active, pas le résultat d'un scan des paquets installés.
Il est écrit uniquement par des commandes explicites (forge opt-in:enable / disable), jamais par découverte automatique (principe 3 : refuser la magie cachée).
Répartition des rôles, calquée sur Symfony (première question de design tranchée) :
requirements.txtrépond à « qu'est-ce qui est installé par pip ? » (équivalentcomposer.json) ;optins/registry.pyrépond à « quels opt-ins le projet utilise ? » (équivalentconfig/bundles.php).
Le recouvrement partiel pour les opt-ins library (installés et déclarés) est assumé : c'est exactement le modèle Symfony (un bundle figure dans composer.json et dans bundles.php).
La redondance est acceptable parce que les deux fichiers répondent à deux questions différentes et que le registre apporte le nom court, le kind, la catégorie et, pour les types qui en ont besoin, le câblage.
Une ligne par opt-in, sémantique selon le kind¶
Chaque opt-in activé apparaît sous une forme déclarative unique, mais la portée de la ligne dépend de son kind (deuxième question de design tranchée) :
route: la ligne porte le câblage réel (agrégation desregister_*_routes, comme aujourd'hui) ;crosscutting: la ligne déclare le point de greffe si l'opt-in en a un ;libraryetcli: la ligne est documentaire (visibilité seule) ; ces opt-ins n'ont rien à câbler, mais leur présence au registre est ce qui rend le projet lisible d'un coup d'œil.
Un opt-in cli comme deploy, qui ne câble rien à l'exécution, obtient donc une ligne documentaire : c'est précisément le but recherché (le voir sans fouiller le .venv).
Le backend BDD figure au registre¶
Le backend BDD choisi (ADR-060) s'inscrit lui aussi au registre, sur une ligne dédiée à sa famille exclusive (ADR-054 : un seul backend par projet).
C'est le point le plus invisible aujourd'hui ; l'y faire figurer répond directement au besoin qui a motivé ADR-060.
Forme illustrative (le format exact relève du ticket) :
# optins/registry.py : registre des opt-ins du projet.
# Écrit et maintenu par « forge opt-in:enable / disable ». Édition manuelle possible.
BACKEND = "sqlite" # forge-mvc-sqlite (ADR-054/060)
ENABLED_OPTINS = {
"qrcode": "library", # forge-mvc-qrcode
"iot": "route", # forge-mvc-iot
"deploy": "cli", # forge-mvc-deploy
}
def register_optins(router):
# câblage des opt-ins « route » activés (inchangé)
...
Écriture sûre, fichier utilisateur¶
Le registre est un fichier du projet, donc du code utilisateur.
Les commandes qui l'écrivent conservent le contrat déjà en vigueur pour opt-in:enable : dry-run par défaut, --apply pour écrire, idempotence, jamais d'écrasement silencieux, aucun import du paquet opt-in (vérification par importlib.util.find_spec).
forge opt-in:list lit ce registre pour afficher l'état réel des opt-ins de tous les kind, sans scanner le .venv.
Conséquences¶
Positives :
- Un fichier unique répond à « quels opt-ins ai-je ? », d'un coup d'œil, sans
pip listni fouille du.venv. - Traitement homogène de tous les
kind: la disparité route / library / cli disparaît. - Le backend BDD redevient visible, ce qui referme le trou de découvrabilité ouvert par ADR-060.
- Fidèle au modèle Symfony correctement compris : code dans le
.venv, lisibilité par registre déclaratif. - Explicite et auditable (principe 3) : pas de découverte magique, le registre est du code lisible.
Coûts et limites :
- Un nouveau fichier de projet à générer et maintenir ;
opt-in:enable/disable/listdoivent être étendus à tous leskind. - Recouvrement partiel avec
requirements.txtpour les opt-inslibrary: tension apparente avec le principe 11, levée par la séparation des rôles (installé vs utilisé), mais à documenter clairement pour éviter la confusion. - Le registre peut diverger du
.venv(opt-in déclaré mais non installé, ou l'inverse) :forge doctordoit signaler l'incohérence. - Les opt-ins
library/cligagnent une ligne documentaire sans effet à l'exécution : utile pour la lisibilité, mais il faut éviter de laisser croire qu'elle « active » un câblage.
Trajectoire¶
- Format du registre : figer la structure de
optins/registry.pycouvrant les quatrekindplusBACKEND. opt-in:enable/disable: inscrire et retirer toute forme d'opt-in au registre, pas seulement lesroute.opt-in:list: afficher l'état d'après le registre pour tous leskind.- Backend au registre :
forge db:*(ou l'installation du backend) inscritBACKEND, en cohérence avec ADR-060. forge doctor: détecter les divergences registre vs.venv(déclaré non installé, installé non déclaré).- Documentation : onboarding et guidance agent (ADR-047) présentent
optins/registry.pycomme la vue d'ensemble des opt-ins du projet.
Alternatives rejetées¶
Copier le source de l'opt-in dans un dossier packages/ du projet (vendoring visible).
Rejetée : contredit le principe 9 (écriture de code framework dans le projet), casse la gestion des versions par pip (plus de correctifs), duplique le code et défait le packaging PyPI (ADR-005).
Ce n'est d'ailleurs pas ce que fait Symfony : son code de bundle reste dans vendor/.
Ne rien ajouter ; ériger requirements.txt en manifeste et enrichir opt-in:list d'un scan des paquets installés.
Plus léger, mais ne donne aucune vue dans l'arborescence du projet (le besoin exprimé), mélange opt-ins et dépendances techniques, et rouvre la tension du scan du .venv que opt-in:list refuse par principe.
Découverte automatique des opt-ins installés par entry points, sans registre.
Rejetée : c'est de la magie cachée (principe 3) ; l'activation d'un opt-in doit rester un acte explicite et visible dans le projet.
Charte appliquée¶
Principe 3 (refuser la magie cachée), principe 8 (noyau minimal, briques opt-in), principe 9 (pas d'écriture invisible dans le code utilisateur), principe 11 (une seule façon officielle de faire chaque chose, via la séparation des rôles installé/utilisé), ADR-016 (modèle opt-in install/enable), ADR-054 (backends BDD), ADR-055 (catégories d'opt-ins), ADR-060 (squelette sans backend).