Aller au contenu

Roadmap Forge 1.0.0-rc.8 - Amélioration des opt-ins

Accueil Retour

Cette roadmap cadre le cycle rc8, dernier cycle avant la 1.0.0 stable.
Elle couvre les vingt-quatre axes d'amélioration relevés sur les opt-ins et sur les backends.

Le périmètre est fixé.
Tous les tickets listés ici sont à livrer avant le tag stable.

Statut : cadrage validé, exécution en cours.
Version de départ : 1.0.0rc7.


1. Pourquoi cette roadmap existe

Une revue des vingt-sept paquets a produit une liste d'améliorations par opt-in.
Confrontée au dépôt, cette liste s'est révélée partiellement périmée.

Seize améliorations demandées étaient déjà livrées.
Le cas le plus net est forge-mvc-admin, décrit comme un paquet vide alors qu'il porte 1259 lignes et dix fichiers de test.

L'erreur ne venait pas de la revue mais du dépôt.
Le README du paquet et sa roadmap de cadrage affirmaient tous deux qu'aucun code n'existait.

C'est le deuxième motif du pré-mortem rc7, la documentation qui affirme ce que le code ne fait plus.
Ce cycle commence donc par rétablir la vérité avant d'ajouter quoi que ce soit.


2. Ce qui était déjà livré

Ces seize points sont retirés du périmètre.
Ils sont consignés ici pour que la prochaine revue ne les redemande pas.

Opt-in Amélioration demandée Où elle vit déjà
admin CRUD générique, auth, CSRF, RBAC, pagination registry.py, resources.py, query.py, http.py
entities Suppression logique déclarée au contrat ADR-083, field_resolver.py, entity.schema.json
mfa Chiffrement du secret TOTP au repos secret_crypto.py, SEC-MFA-SECRET-ENCRYPTION-001
mfa Limitation de débit sur le challenge AUTH_RATE_LIMIT_MFA_CHALLENGE, cinq essais
sessions-db Purge planifiable commande sessions:gc
jobs Réessai avec délai backoff_seconds, dix secondes à six cents
notifications Tout marquer comme lu mark_all_read, unread_count
notifications Types de notification déclarés colonne type
images Variantes nommées IMAGE_VARIANT_SIZES
video Vignette automatique et métadonnées poster_path, duration_seconds, width, height
video Suivi d'état uploaded, processing, ready, failed
mail Journal des envois en base table mail_log, commande mail:logs
settings Typage déclaré colonne value_type
audit Rétention configurable et purge purge_audit_before, commande audit:gc
stats Agrégations SQL aggregate.py, get_stats_counts_sql
qrcode Aide contrôleur QrCodeResponse.from_text
fixtures Refus de charger en production sans --force ADR-074, load.py, purge.py

3. Lot 0 - Rétablir la vérité

Ce lot passe en premier.
Il est petit, et il empêche la prochaine revue de repartir d'un état faux.

Ticket Responsabilité unique
ADMIN-DOC-ETAT-REEL-001 Livré. Le README annonçait « à venir » des filtres livrés et des actions groupées désormais livrées
META-README-COMMANDS-RATCHET-001 Livré. Refuse une commande promise et absente, et une commande livrée annoncée « à venir »

4. Lot 1 - Sécurité et pertes de données

Ces tickets corrigent des manques qui produisent une panne ou une fuite.
Ils passent avant tout ajout de confort.

Ticket Responsabilité unique
FIXTURES-APP-ENV-GUARD-001 Retiré, faux besoin. La garde existait déjà (ADR-074). Remplacé par ENV-APP-ENV-NORMALISATION-001, qui corrige la vraie faille, la casse qui la désarmait
ENV-APP-ENV-NORMALISATION-001 Livré. Lecture unique et normalisée de APP_ENV dans core.app.env
MFA-KEY-ROTATION-001 Livré. FORGE_MFA_SECRET_KEY_PREVIOUS, rotate_totp_secret, uses_current_key
SESSIONS-DELETE-FOR-USER-001 Livré. delete_for_user au contrat et aux trois stores, plus AddColumn pour faire évoluer un schéma d'opt-in
MFA-SESSION-INVALIDATION-001 Livré. except_session_id épargne la session courante, geste documenté et exercé
IOT-RETENTION-GC-001 Livré. iot:gc --days N, nommé comme les trois autres purges et non iot:purge
DEPLOY-CHECK-SECRETS-001 Livré. Repérage par nom de variable, valeur jamais affichée, liste partagée dans core.security.secrets
FILES-METADATA-TABLE-001 Livré. Table forge_files et registre explicite, ADR-094 amendant l'ADR-020

5. Lot 2 - Socles dont d'autres tickets dépendent

Ticket Responsabilité unique
I18N-LOCALE-DETECTION-001 Livré. detect_locale, liste blanche obligatoire, négociation par facteurs de qualité
JOBS-PRIORITY-001 Livré. priority DESC, id, niveaux nommés, AddColumn étendu aux index composites
JOBS-STATUS-CLI-001 Livré. Compteurs par file, colonne « prêtes » distincte de « en attente », lecture seule
ADMIN-LIST-FILTERS-001 Livré. Déclaration obligatoire, noms vérifiés en liste blanche, ORDER BY portable
ENTITIES-UNIQUE-COMPOSITE-001 Livré. Les index déclarés atteignent le SQL, contrainte nommée, doublon refusé sur serveurs réels
RBAC-INSTANCE-PERMISSIONS-001 Livré. has_instance_permission, ordre global puis propriétaire, composition et non quatrième niveau
WORKFLOW-HOOKS-001 Livré. apply_transition, veto par exception, commit fourni par l'application
DB-DOCTOR-001 Livré. Enrichit forge doctor plutôt que d'ouvrir db:doctor (principe 11) : version, encodage, base, compte

6. Lot 3 - Compositions entre opt-ins

Ces tickets branchent des opt-ins les uns sur les autres.
Chacun dépend d'un ticket du lot 2.

Ticket Dépend de Responsabilité unique
MAIL-QUEUE-VIA-JOBS-001 jobs Livré. MAIL_JOB_TASK, sérialisation JSON, gestionnaire ; aucune dépendance croisée
IMPEXP-ASYNC-JOBS-001 jobs Livré. Importeurs nommés, racine de chemins obligatoire, lignes invalides non fatales
RBAC-DENIAL-AUDIT-001 audit Livré. Les 3 gardes annoncent, observateurs isolés, aucune dépendance croisée
AUDIT-CSV-EXPORT-001 import-export Livré. Export non borné, curseur par identifiant, aucune dépendance croisée
NOTIF-MAIL-BRIDGE-001 mail, jobs Livré. on_notification_created, relais isolés, pont à trois paquets sans dépendance
ADMIN-SETTINGS-UI-001 admin Livré. Conversion de saisie explicite, forme renvoyable, aucune dépendance au back-office
ADMIN-SESSIONS-VIEW-001 admin Livré. list_for_user sur les 3 stores, résumé sans jeton, préfixe non révocable
WORKFLOW-ENTITY-STATUS-001 entities Livré. Le contrat devient la source, champ nommé jamais deviné, aucune dépendance

7. Lot 4 - Complétude par opt-in

7.1 admin

Ticket Responsabilité unique
ADMIN-BULK-ACTIONS-001 Livré. Suppression et transitions groupées, câblées de la case à cocher au workflow. La première livraison n'avait posé que la fonction de requête, inatteignable depuis le back-office

7.2 entities

Ticket Responsabilité unique
ENTITIES-COMPUTED-FIELDS-001 Livré. Projetés par leur expression, absents des écritures ; quatre combinaisons refusées
ENTITIES-BUSINESS-VALIDATION-001 Livré. Fonction enregistrée, pas de mini-langue ; toutes les règles évaluées
ENTITIES-SLUG-ROUTES-001 Livré. La recherche existait, la route non ; déclarée en dernier
ENTITIES-MIGRATION-DIFF-READABLE-001 Livré. Résumé, --sql sans écrire, --check pour la CI

7.3 rbac

Ticket Responsabilité unique
RBAC-CONTRACT-EXPORT-001 Livré. Markdown et CSV ; rend le contrat, jamais l'état de la base

7.4 workflow

Ticket Responsabilité unique
WORKFLOW-HISTORY-001 Livré. Table dédiée ; enregistrement explicite, dans la transaction de l'appelant
WORKFLOW-CONDITIONS-001 Livré. Une condition dit pourquoi elle refuse ; une panne refuse

7.5 mfa

Ticket Responsabilité unique
MFA-REQUIRED-BY-ROLE-001 Livré. Trois emplacements de rôles lus ; ne lève jamais, n'active rien

7.6 sessions-db

Ticket Responsabilité unique
SESSIONS-TTL-PER-KIND-001 Livré. Trois natures fermées ; valeur illisible refusée
SESSIONS-ACTIVE-METRIC-001 Livré. Filtre en SQL ; une session expirée n'est pas active
SESSIONS-GC-TIMER-DOC-001 Livré. Deux unités, Persistent, dispersion, et le piège du .service

7.7 jobs

Ticket Responsabilité unique
JOBS-IDEMPOTENCY-KEY-001 Livré. Index unique dialectal : SQL Server n'accepte qu'un seul NULL
JOBS-HEARTBEAT-001 Livré. heartbeat(claim_token), gardé par le jeton
DOC-JOBS-COMPOSITION-001 Livré. Tableau des motifs, et ce qui doit faire échouer une tâche

7.8 mail

Ticket Responsabilité unique
MAIL-ATTACHMENTS-001 Livré. Message immuable, nom de fichier assaini, type jamais deviné faux
MAIL-TEST-GUIDED-001 Livré. --dry-run, diagnostic avant l'envoi
MAIL-LAYOUTS-001 Livré. Faux besoin : l'héritage Jinja marchait déjà ; capacité figée par des tests et documentée

7.9 notifications

Ticket Responsabilité unique
NOTIF-TARGET-URL-001 Livré. Colonne dédiée validée à l'écriture, schémas exécutables refusés
NOTIF-PAGINATION-001 Livré. Curseur before_id, jamais d'OFFSET
NOTIF-HTTP-ROUTES-001 Livré hors cycle, voir section 11. Quatre routes JSON, destinataire résolu par l'application et jamais lu dans la requête

7.10 files

Ticket Responsabilité unique
FILES-QUOTA-001 Livré. Par couple propriétaire, valeur d'env illisible refusée, fenêtre de concurrence documentée
FILES-SCAN-HOOK-001 Livré. Consultée avant écriture, une panne refuse le dépôt au lieu de le laisser passer
FILES-ORPHAN-PURGE-001 Livré. Deux sortes d'orphelins, registre vide et fichier récent interrompent
DOC-FILES-XACCEL-001 Livré. Motif documenté, internal; posé comme la moitié qui protège

7.11 images

Ticket Responsabilité unique
IMAGES-PRESETS-DECLARATIFS-001 Livré. IMAGE_VARIANTS, lus à chaque appel, nom réservé et doublon refusés
IMAGES-FOCAL-CROP-001 Livré. Fenêtre recalée dans l'image, aucun pixel inventé
IMAGES-ORPHAN-VARIANTS-001 Livré. Deux catégories, sans base ; images:orphans affiche puis supprime
IMAGES-ENTITY-FIELD-001 Livré. variants accepte une liste ; nom non déclaré refusé à la génération
IMAGES-LIMITS-CONFIG-001 Livré. Largeur, hauteur et poids ; valeur illisible refusée

7.12 video

Ticket Responsabilité unique
VIDEO-STATUS-UI-001 Livré. Vue d'état, route JSON ; la sortie ffmpeg ne sort jamais
VIDEO-QUOTA-001 Livré. Les limites par fichier existaient ; les plafonds cumulés manquaient
VIDEO-SUBTITLES-001 Livré. Table dédiée, WebVTT seul, signature vérifiée à l'entrée

7.13 audio

Ticket Responsabilité unique
AUDIO-ID3-001 Livré. ffprobe les rendait déjà ; nettoyées, elles viennent du fichier envoyé
AUDIO-TRIM-001 Livré. audio:trim ; ni la source ni une sortie existante ne sont écrasées
AUDIO-DOCTOR-HARMONISE-001 Livré. Faux besoin : déjà alignés. Garde-fou posé, et la vraie divergence corrigée (config)

7.14 iot

Ticket Responsabilité unique
IOT-DEVICE-AUTH-001 Livré. Le jeton unique ouvrait TOUS les sites ; portées, empreintes, révocation
IOT-AGGREGATES-001 Livré. En SQL, fenêtre vide distincte d'une moyenne nulle
IOT-RBAC-READ-001 Livré. Prise, pas dépendance ; une vérification en panne refuse

7.15 import-export

Ticket Responsabilité unique
IMPEXP-COLUMN-MAPPING-001 Livré. source déclaré ; colonne absente = 1 erreur, plus 10 000
IMPEXP-ERROR-REPORT-001 Livré. CSV échappé, ligne tableur et valeur refusée
IMPEXP-FILTERED-EXPORT-001 Livré. Faux besoin : les filtres passaient déjà. La troncature silencieuse corrigée
IMPEXP-JSONL-001 Livré. Types préservés, clé absente à null, lecture stricte par défaut

7.16 fixtures

Ticket Responsabilité unique
FIXTURES-SCENARIOS-001 Livré. Sous-dossiers ; un nom inconnu lève au lieu de charger zéro
FIXTURES-SNAPSHOT-001 Livré. Affiche d'abord, refuse en prod, plafond bas, en-tête qui prévient
FIXTURES-FK-ORDER-ROBUST-001 Livré. Toutes les tables d'un fichier ; le repli est dit, plus silencieux

7.17 settings

Ticket Responsabilité unique
SETTINGS-PER-USER-001 Livré. Préfixe user. réservé, collision refusée, pas de repli sur le global
SETTINGS-CACHE-001 Livré. Éteint par défaut, invalidation explicite, absence mise en cache
DOC-SETTINGS-NO-SECRETS-001 Livré. Limite tracée, et pourquoi chiffrer la table ne résoudrait rien

7.18 audit

Ticket Responsabilité unique
AUDIT-FILTERS-001 Livré. Les 4 filtres d'égalité existaient ; période ajoutée, date de fin incluant la journée
AUDIT-ACTION-HELPER-001 Livré. record_request_audit, acteur pris en session, jamais bloquant

7.19 stats

Ticket Responsabilité unique
STATS-IP-ANONYMISATION-001 Livré. Aucune adresse n'était stockée ; metadata était la porte, elle est fermée
STATS-EVENT-KIND-001 Livré. Vocabulaire fermé, colonne kind, migration additive
DOC-STATS-AGGREGATES-001 Livré. L'agrégation par jour n'existait pas ; Dialect.date_expression la porte

7.20 qrcode

Ticket Responsabilité unique
QRCODE-CLI-001 Livré. Affiche puis écrit ; n'écrase jamais, refuse une extension contradictoire
QRCODE-ERROR-LEVEL-001 Livré. Il existait sur le générateur, la réponse HTTP ne le transmettait pas

7.21 i18n

Ticket Responsabilité unique
I18N-EXTRACT-CLI-001 Livré. Le cas que i18n:check ne peut pas voir ; clé calculée comptée à part
I18N-MISSING-KEYS-DEV-001 Livré. Journal et registre hors prod, une fois par clé, sans jamais lever
I18N-PLURALS-001 Livré. Deux formes ; les langues qui en demandent plus lèvent au lieu de mentir

7.22 deploy

Ticket Responsabilité unique
DEPLOY-NGINX-MEDIA-HEADERS-001 Livré. HSTS que le cœur délègue au proxy, nosniff sur /static/, internal;
DEPLOY-TIMERS-DOC-001 Livré. Deux unités complètes et quatre pièges nommés

7.23 backends

Ticket Responsabilité unique
DB-ERROR-MESSAGES-HOMOGENES-001 Livré. La clé étrangère n'était pas qualifiée ; vérifié sur les quatre serveurs
DOC-DIALECT-ECARTS-001 Livré. Bornes, booléens, insertion conditionnelle, erreurs, et ce qui reste hors contrat

7.24 testing

Ticket Responsabilité unique
TESTING-CLIENT-001 Livré. Passe par le vrai callable WSGI, pas par un jumeau
TESTING-LOGIN-AS-001 Livré. Vrai magasin de sessions ; logout détruit vraiment
TESTING-FIXTURES-ALIGN-001 Livré. Réutilise le code du paquet, pas une seconde implémentation
TESTING-ASSERTIONS-001 Livré. Messages qui nomment la cause ; la rotation exige l'ancienne morte

8. Lot 5 - Tickets en tension avec la charte

Ces cinq tickets sont demandés, et chacun entre en tension avec un principe.
La tension est nommée ici pour être tranchée en connaissance de cause, non pour bloquer la livraison.

Ticket Tension Décision
MFA-WEBAUTHN-001 Principe 8, le facteur TOTP suffit au socle et la charge de maintenance est durable Retiré du périmètre
RBAC-ROLE-HIERARCHY-001 Limite assumée par l'ADR-014, la rouvrir élargit le contrat public Livré, ADR-095
AUDIO-STATEFUL-OPTION-001 Principe 11, une seconde façon de gérer un média avec état existe déjà dans video Retiré du périmètre
DEPLOY-CADDY-001 Principe 11, deux serveurs frontaux officiels au lieu d'un Retiré du périmètre
NOTIF-POLLING-HELPER-001 Principe 1, le rafraîchissement d'interface relève de l'application Retiré du périmètre

Décisions écrites, 2026-09-03

Roger a tranché les cinq en connaissance de cause, comme ce lot le prévoyait.
Un ticket est livré, quatre sont retirés du périmètre.
Le critère de clôture du cycle admet le retrait par décision écrite, et ces paragraphes en tiennent lieu.

RBAC-ROLE-HIERARCHY-001, livré.
La tension était réelle mais bornée : la fonction est petite, la demande est constante, et l'ADR-014 citait déjà l'héritage de permissions comme relevant d'un contrat RBAC.
La limite levée était une limite d'implémentation, non une frontière de conception.
L'élargissement du contrat public est cadré par l'ADR-095.

MFA-WEBAUTHN-001, retiré.
WebAuthn est une spécification large et mouvante, qui demande une bibliothèque à suivre, une gestion d'attestation, et des contournements par navigateur.
Le principe 8 vise exactement cette charge : un noyau minimal ne porte pas ce que sa maintenance ne peut pas garantir sur la durée.
Le facteur TOTP couvre le socle, et une application qui a besoin de WebAuthn l'implémente avec une bibliothèque dédiée, sans que Forge prétende le faire à sa place.

AUDIO-STATEFUL-OPTION-001, retiré.
Une seconde façon de gérer un média avec état existe déjà dans forge-mvc-video, et le principe 11 en veut une seule.
Si le besoin revient, la bonne réponse n'est pas de dupliquer la machinerie dans audio mais de l'extraire en socle partagé.
C'est un ticket d'architecture, à instruire quand deux besoins indépendants l'auront demandé, pas une option à ajouter aujourd'hui.

DEPLOY-CADDY-001, retiré.
Un second gabarit officiel doit être maintenu, testé et tenu à jour au même rythme que le premier, et le principe 11 refuse deux façons officielles de faire la même chose.
La documentation peut nommer Caddy comme alternative viable sans que Forge en engendre la configuration : dire qu'une chose est possible ne coûte rien, la maintenir coûte à chaque version.

NOTIF-POLLING-HELPER-001, retiré.
Le rafraîchissement d'un écran relève de l'application, principe 1.
Forge livre déjà HTMX, avec lequel un rafraîchissement périodique tient en un attribut.
Ajouter un assistant ne retirerait aucune décision à l'application, il en masquerait une.

Ce motif portait une affirmation fausse, corrigée ici.
Il disait que « la route JSON que l'aide aurait appelée existe déjà côté notifications », et le paquet n'exposait alors aucune route.
Le retrait tient, la marche manquante n'était pas l'assistant mais la route, livrée par NOTIF-HTTP-ROUTES-001 (section 11).


9. Compte et ordonnancement

Lot Tickets
Lot 0, vérité 2
Lot 1, sécurité et pertes 6
Lot 2, socles 8
Lot 3, compositions 8
Lot 4, complétude 56
Lot 5, tension 5
Total 85

L'ordre des lots est une contrainte, pas une préférence.
Un ticket du lot 3 ne peut pas être livré avant le ticket du lot 2 dont il dépend.

À l'intérieur d'un lot, l'ordre est libre.


10. Critères de clôture du cycle

Le cycle rc8 est clos quand les six conditions suivantes sont vraies.

  • Les quatre-vingt-cinq tickets sont livrés ou explicitement retirés du périmètre par décision écrite.
  • La suite complète passe, marqueurs docs compris.
  • mkdocs build --strict passe.
  • Chaque paquet touché voit sa documentation embarquée mise à jour dans le même commit que le code.
  • Aucun README d'opt-in ne décrit un état antérieur à son code, garde-fou META-README-COMMANDS-RATCHET-001 vert.
  • Le CHANGELOG.md porte une entrée par ticket livré.

11. Après la clôture

Le cycle est clos, et cette section recueille ce que sa revue a fait apparaître ensuite.

Elle n'en rouvre pas le périmètre.
Chaque entrée nomme ce qui l'a révélée, parce qu'un ticket sans cause visible se relit mal.

Ticket Paquet Révélé par État
ADMIN-BULK-ACTIONS-001, câblage admin Revue de la proposition admin Livré. La première livraison n'avait posé que la fonction de requête, inatteignable depuis le back-office
ADMIN-DOC-ETAT-REEL-001 admin Même revue Livré. Le README annonçait « à venir » des capacités déjà présentes
NOTIF-HTTP-ROUTES-001 notifications Besoin d'asynchrone d'une application réelle Livré. Quatre routes JSON, marquage borné au destinataire
JOBS-WORKER-GRACEFUL-STOP-001 jobs Écriture de l'unité systemd du worker Livré. stop était sans effet sur une file chargée
DEPLOY-JOBS-WORKER-UNIT-001 deploy Mesure des besoins d'asynchrone Livré. Le worker n'existait nulle part sur le chemin de production
DEPLOY-CHECK-JOBS-WORKER-001 deploy Même mesure Livré. Le pré-vol ne regardait pas si quelqu'un traitait la file
DEPLOY-NGINX-RATE-LIMIT-001 deploy Même mesure Livré. La parade prescrite n'était pas dans la configuration engendrée
MFA-RATE-LIMIT-SHARED-STORE-001 mfa Même mesure Retiré du périmètre, voir ci dessous
DB-POOL-THREADS-DOC-001 cœur Même mesure Livré. Ce que devient la base quand une requête ouvre des threads
META-DOC-ABSOLUTE-LINKS-001 méta Un lien cassé accepté par le build strict Livré. Les liens absolus échappaient à mkdocs --strict
META-README-RATCHET-WIDEN-001 méta Dix paquets sur vingt-sept sautés Livré. Le cliquet ne lisait ni les blocs de code ni les citations nues, et sa règle était fausse
SKELETON-TAILWIND-CSS-STALE-001 squelette Enquête sur le coût de forge new Livré. Le CSS versionné manquait quinze classes de ses propres gabarits
FORGE-NEW-NO-NODE-DEFAULT-001 CLI Même enquête Livré. forge new passe de 144 s à 5,3 s ; --with-node pour l'ancien comportement
ENTITIES-COMPUTED-CANONICAL-001 entities Revue du référentiel entities Livré. Les champs calculés n'étaient déclarables que dans le format interne ; le normaliseur perdait l'expression en silence
RBAC-DENIAL-AUDIT-COMPLETE-001 rbac Revue du référentiel rbac Livré. L'audit des refus couvrait 3 gardes sur 5, et manquait la canonique
WORKFLOW-CONDITIONS-APPLIED-001 workflow Revue du référentiel workflow Livré. apply_transition ne consultait pas le registre de conditions
OPTINS-MATURITY-FOLLOWS-CORE-001 tous Consigne : plus aucun opt-in en bêta Livré. Dix fichiers s'attribuaient un stade propre, dont des affirmations fausses sur le MFA
PKG-PYRIGHT-FIXTURES-001 fixtures Relecture du pyproject.toml racine Livré. Le paquet n'était vérifié par aucun typage, alors que ses dix fichiers portent # pyright: strict
SESSIONS-TTL-AUTHENTICATED-APPLIED-001 sessions-db Revue du référentiel sessions-db Livré. SESSION_TTL_AUTHENTICATED était sans effet : ttl_for() n'était lue que par create()
ADMIN-SESSIONS-VIEW-001 admin Même revue Livré. La métrique existait, l'écran non ; couplage souple, aucun identifiant exposé
JOBS-HEARTBEAT-REACHABLE-001 jobs Revue du référentiel jobs Livré. Le gestionnaire ne recevait pas son jeton ; l'exemple documenté cassait la tâche
MAIL-QUEUE-ATTACHMENTS-REFUSED-001 mail Revue du référentiel mail Livré. Pièces jointes et file ne composaient pas ; la pièce disparaissait en silence
IMAGES-REGISTRY-RECORD-001 images, files Revue du référentiel files Livré. files:orphans --delete supprimait les images et leurs vignettes
NOTIF-STORE-AS-VALIDATED-001 notifications Revue du référentiel notifications Livré. Destinataire validé élagué et stocké brut : notification écrite, jamais relue
DOC-FILES-RETENTION-SCOPE-001 files Revue du référentiel files, deuxième passe Livré. L'absence de purge par ancienneté était silencieuse ; elle est motivée et le chemin applicatif donné
FILES-DELETE-FORGETS-001 files, images Revue du référentiel images Livré. Trois chemins de suppression laissaient la ligne au registre ; le quota comptait des fichiers supprimés
VIDEO-POSTER-ROUTE-001 video Revue du référentiel video Livré. Vignette et métadonnées inscrites en base, sans route ni exposition
IMPEXP-JOB-ROOT-REQUIRED-001 import-export Revue du référentiel import-export Livré. La racine annoncée obligatoire ne l'était pas : lire tout le disque depuis une ligne de file

NOTIF-HTTP-ROUTES-001.
Le paquet savait écrire une notification et la relire depuis Python, et n'exposait aucune route.
forge-mvc-video livre register_video_routes, forge-mvc-iot livre register_iot_routes, celui ci ne livrait rien.

Chaque application devait donc écrire son contrôleur, sa sérialisation et son compteur de non-lus avant d'afficher quoi que ce soit.
Mesuré sur une application réelle, elle appelait notify() depuis des mois sans avoir jamais affiché une seule notification, ayant buté sur cette marche.

Le point qui décide de tout le reste est le destinataire.
Il est résolu par l'application, jamais lu dans la requête, et l'absence du résolveur empêche l'enregistrement des routes.

Le marquage est borné au même destinataire, ce qui a demandé d'ajouter recipient à mark_read avant d'exposer quoi que ce soit.
Sans cette borne, l'identifiant seul suffisait à faire disparaître l'alerte de quelqu'un d'autre.

MFA-RATE-LIMIT-SHARED-STORE-001, retiré.
Je l'avais proposé, puis mesuré, et la mesure a renversé la recommandation.

Le compteur de tentatives vit en mémoire de processus, et vaut donc 5 × N travailleurs.
La conclusion semblait couler de source : livrer un magasin partagé adossé à la base, sur le modèle de DbTotpReplayStore.

Trois faits s'y opposent.

Le premier est que la parade tient ailleurs, et mieux.
Une fois limit_req posé au proxy, cinq POST par minute et par IP, le nombre de travailleurs ne change plus rien à ce qu'un attaquant peut tenter à travers lui.
Le multiplicateur devient inatteignable, et il l'est pour la connexion comme pour le challenge.

Le deuxième est que la couture existe déjà.
check_auth_rate_limit accepte une liste de tentatives chargée d'où l'application veut : celle qui exige un compteur applicatif partagé peut l'écrire sans que Forge grossisse.

Le troisième est le principe 8.
Un protocole, une table, une migration et un magasin de plus dans un chemin de sécurité coûtent à chaque version, pour un besoin qui est une hypothèse de ma part et non un retour de terrain.
C'est la règle des deux occurrences indépendantes, celle qui a écarté quatre tickets du lot 5.

La moitié utile du ticket restait la révélation, règle B, et elle est livrée : la documentation du MFA ne s'appuie plus sur un contrôle affaibli de la même façon que celui qu'elle relativise.