Leganews Pro
Un corpus juridique congolais versionné avec relecture éditoriale, recherche, veille et accès par abonnement
Rôle
Cofondateur et CTO
Un corpus juridique congolais versionné avec relecture éditoriale, recherche, veille et accès par abonnement
Cofondateur et CTO
2025
Collecte de l’actualité congolaise avec un collecteur Rust, un tableau de bord TanStack Start et une base d’articles consultable
Indiquez l’objectif, la pile technique et l’échéance. Je vous répondrai avec une prochaine étape.
Leganews a commencé en 2019 autour d’un problème récurrent pour les professionnels du droit en République démocratique du Congo : trouver un texte demandait souvent de parcourir des publications officielles, des sites institutionnels, des fichiers numérisés et des archives privées. Obtenir une copie n’était qu’une première étape. Il fallait encore identifier sa source, la version pertinente et les textes qui s’y rapportaient.
En tant que cofondateur et CTO, je travaille sur l’orientation du produit et sur le logiciel qui accompagne cette recherche. La première application en production gérait déjà les contenus juridiques, les abonnements, les paiements, les favoris et l’historique des recherches. La plateforme actuelle doit préserver ce travail tout en prenant en charge un corpus plus précisément modélisé, la relecture éditoriale, la veille et la comparaison de documents.
Les derniers changements dépassent l’interface utilisateur. La recherche utilise désormais Meilisearch avec PostgreSQL en secours. Les alertes couvrent les versions publiées et les annexes ajoutées. Redis conserve les sessions au-delà des redémarrages applicatifs, et les déploiements enregistrent précisément les images utilisées.
La première plateforme utilisait Symfony, Twig, Symfony UX et MariaDB. Son modèle Content réunissait un corps courant, des métadonnées de publication, des indicateurs d’accès et des compteurs. Il permettait de publier une fiche, mais un texte juridique devait conserver son identité lorsque sa rédaction changeait ou qu’un nouveau fichier lui était associé.
J’ai réparti ces responsabilités entre quatre concepts :
| Concept dans le code | Responsabilité |
|---|---|
| Work, ou texte | L’identité stable, la référence et les métadonnées d’un texte normatif ou d’une décision de justice |
| Expression | Une version du texte, avec ses dates, sa période de validité et son état éditorial |
| Expression Unit, ou unité d’expression | Un article, un chapitre, un paragraphe ou une autre partie structurée de cette version |
| Manifestation, ou restitution | Une représentation destinée à la lecture ou à l’export, notamment en HTML ou en PDF |
Une annexe documentaire appartient au texte et reste distincte d’une version de son contenu. Les domaines, matières, juridictions, types et sources ont également des sens différents. Le sujet juridique et l’institution à l’origine d’un texte ne doivent pas se disputer un même champ de catégorie.
Ce modèle est implémenté. Le texte conserve sa page pendant que le lecteur choisit une expression. Un éditeur peut préparer un nouveau brouillon sans retirer la version déjà publiée. Les dates, sources et relations enregistrées aident à examiner le texte ; sa publication dans Leganews ne certifie ni son maintien en vigueur ni l’exhaustivité du corpus.
L’application d’administration comprend un éditeur Lexical adapté au texte juridique. Lors d’un enregistrement, le backend remplace les références de l’éditeur par des identifiants stables, valide la structure selon la nature du texte, puis conserve le document d’édition et l’arbre des unités dans une même transaction. Chaque unité possède un parent, une position et un chemin matérialisé. Le corps constitue l’unique racine.
Le document d’édition et les unités structurées répondent à des besoins différents. L’équipe éditoriale utilise la mise en forme et l’état de l’éditeur. La lecture, les citations, les exports et les comparaisons ont besoin de parties identifiables dans une version juridique. Pour commencer une expression continue, la plateforme peut copier la structure existante et réécrire ses références internes afin que la nouvelle version ne pointe pas vers les unités de l’ancienne.
Le parcours éditorial distingue brouillon, révision, approbation, publication et retrait. Un relecteur peut demander des corrections en les accompagnant d’un commentaire. L’état éditorial public du texte découle de ses expressions : une révision non publiée ne masque donc pas une version plus ancienne déjà disponible.
La publication déclenche notamment la préparation du résumé, l’actualisation de la recherche, les comparaisons de versions et la recherche d’alertes correspondantes. L’enregistrement de citations structurées peut aussi créer des relations entre textes. Ces relations distinguent une citation d’une modification, d’une abrogation ou d’un texte d’application. Les suggestions automatiques conservent leur propre état de validation et ne deviennent pas, par défaut, des relations confirmées.
Pour les résumés, la plateforme conserve une provenance manuelle, générée ou extraite. L’adaptateur Symfony AI accepte un fournisseur OpenAI, Gemini ou Ollama configuré et limite la quantité de texte transmise au modèle. Il demande un court résumé factuel en français, sans interprétation. En cas d’échec du fournisseur, un résumé extrait peut prendre le relais. Le prompt et l’indication de provenance ne suffisent toutefois pas à établir l’exactitude d’un texte généré.
La découverte publique appartient au contexte Corpus. Son implémentation de recherche utilise encore des adaptateurs dans Monitoring, qui conserve également les recherches récentes et les suggestions de mots-clés. Cette distinction compte dans la description du code : la responsabilité produit s’est précisée sans que toutes les classes aient été déplacées simultanément.
Meilisearch assure maintenant la recherche principale. Le lecteur peut combiner une requête avec des filtres juridiques, choisir une portée de recherche et examiner les passages mis en évidence. PostgreSQL conserve des documents de recherche persistants et une implémentation de secours. Si la requête Meilisearch échoue, l’adaptateur journalise l’erreur et interroge PostgreSQL. Une réponse Meilisearch réussie mais périmée ne déclenche pas ce repli.
L’actualisation des projections écrit d’abord le document PostgreSQL, puis tente Meilisearch. Un échec Meilisearch est journalisé sans supprimer la projection en base. La réindexation peut reconstruire l’index externe à partir de ces documents. Ce fonctionnement ajoute un retard d’indexation possible et des opérations de réparation. La reconstruction ordinaire vide l’index Meilisearch avant de charger les lots ; elle ne constitue donc pas un échange d’index sans interruption.
J’ai également séparé les données réutilisables en cache des lectures propres à l’utilisateur. Les référentiels, répertoires publics, compteurs agrégés et restitutions peuvent être mis en cache dans Redis. Les clés des restitutions incluent les révisions du contenu : une modification sélectionne une autre entrée. Les décisions d’accès et les réponses personnalisées complètes restent hors de ce cache. Une défaillance du cache Redis peut entraîner un nouveau calcul ; les sessions et files Redis ont d’autres exigences de disponibilité.
Une recherche juridique se limite rarement à une seule requête. Le lecteur peut enregistrer des textes dans des collections nommées, les Dossiers de l’interface française, conserver des notes personnelles et revenir par les historiques de recherche, de consultation ou de téléchargement. Chaque historique peut être effacé séparément. Supprimer son historique de téléchargement ne réduit pas le compteur global des téléchargements d’un texte.
Monitoring distingue l’alerte de sa notification. L’alerte enregistre ce que l’utilisateur suit : un domaine, une matière ou un texte. La notification conserve l’événement correspondant. La publication d’une expression et l’ajout d’une annexe alimentent désormais le même service de notification.
Le rapprochement sélectionne les abonnés éligibles par lots limités, puis charge leurs alertes correspondantes. Il tient compte de la date de création de l’alerte, exclut les événements déjà notifiés et départage les cibles qui se chevauchent selon les règles du domaine. Les contraintes d’unicité fournissent une dernière protection contre les doublons. Par défaut, le service traite 100 destinataires par lot, enregistre les notifications et planifie leur diffusion en temps réel par Mercure. Les préférences de courriel s’appliquent séparément ; une erreur d’envoi est journalisée sans arrêter les autres destinataires du lot.
Le récapitulatif hebdomadaire constitue un mécanisme distinct de rattrapage pour les utilisateurs éligibles qui ne l’ont pas désactivé. Sa fonction et ses préférences diffèrent d’une notification d’alerte individuelle et des lettres d’information générales.
Compare prend en charge deux parcours : comparer des expressions publiées d’un même texte et comparer deux fichiers PDF ou DOCX téléversés. Le second exige un abonnement actif et autorise une seule comparaison en traitement par utilisateur. L’enregistrement de comparaison expire après sept jours.
Un worker extrait le texte, le normalise et calcule les différences. Des empreintes de fichiers identiques permettent d’éviter l’extraction. Un texte extrait vide ou un résultat trop volumineux produit un échec explicite. Les fichiers temporaires sont supprimés après la réussite ou l’échec du traitement. Les mises à jour Mercure et les courriels de fin ou d’échec informent le lecteur après le retour de la requête initiale.
L’interface présente les différences côte à côte, avec les numéros de ligne, les noms des fichiers et une option d’impression. La comparaison porte sur le texte extrait. L’adaptateur PDF utilise pdftotext : un document numérisé composé uniquement d’images demande une préparation supplémentaire. Le sens juridique d’une différence de rédaction reste à examiner par le lecteur.
Billing conserve les produits, prix, commandes, transactions et événements du prestataire. Access détermine les abonnements, les appartenances aux organisations, les places et les droits directs sur un texte. Je sépare ces responsabilités parce qu’un paiement confirmé doit encore ouvrir le bon droit à la bonne personne ou organisation.
FlexPay prend en charge les parcours mobile money et carte, avec des montants en CDF et en USD représentés par des objets-valeurs monétaires. Une empreinte permet de reconnaître les callbacks répétés. L’exécution verrouille la commande, vérifie ses lignes, délègue à Access la création ou le renouvellement des droits, puis marque la commande comme exécutée après leur réussite. Une exécution déjà réalisée est détectée avant de créer un autre droit.
Le cas d’échec important est celui d’une commande payée dont l’accès n’a pas encore été accordé. L’état du paiement et celui des droits restent examinables séparément, et l’exécution dispose d’une commande explicite. Sa relance doit conserver la commande existante et éviter de dupliquer les abonnements ou les droits.
Access enregistre aussi les dépassements des limites de consultation et de téléchargement dans un dossier d’abus dédié. Les éléments conservés restent distincts de l’état du compte géré par Identity. Le système peut ainsi garder l’historique des avertissements et le motif d’une décision sans transformer l’objet utilisateur en un ensemble de compteurs sans lien entre eux.
Les applications principale et d’administration utilisent React, TanStack Start, TanStack Router et TanStack Query dans un espace de travail Bun. Des paquets partagés fournissent les contrats d’API, l’authentification, les formulaires, le lecteur juridique, l’éditeur et les composants d’interface. Des schémas Zod définissent les contrats TypeScript. Symfony reste responsable des autorisations et des décisions métier.
L’application principale réunit la découverte publique et un espace de recherche authentifié. L’administration prend en charge les opérations éditoriales et commerciales. Chacune possède sa session côté serveur et son proxy d’API de même origine. Le navigateur reçoit un cookie de session HttpOnly ; les jetons d’accès et de renouvellement Symfony restent dans le stockage serveur adossé à Redis.
Dans un même processus applicatif, les requêtes concurrentes d’une session partagent le renouvellement de jeton en cours. Les défaillances temporaires de l’API sont distinguées des identifiants refusés : une indisponibilité n’a donc pas à être interprétée comme un mauvais mot de passe ou une session invalide. Le proxy transmet les réponses en flux et limite les corps de requête mis en mémoire ainsi que le temps d’attente du serveur amont.
Le backend Symfony comprend désormais huit contextes métier :
| Contexte | Responsabilités |
|---|---|
| Corpus | Textes, versions, structure, parcours éditorial et découverte |
| Monitoring | Historiques de recherche, dossiers, notes, alertes et notifications |
| Identity | Comptes, authentification et vérifications de sécurité |
| Access | Droits, organisations, membres et contrôle des accès |
| Billing | Catalogue commercial, commandes et paiements |
| Compare | Comparaisons de documents téléversés et d’expressions |
| Policy | Documents de politique de la plateforme et leurs révisions |
| Reporting | Statistiques transversales et rapports mensuels |
Reporting a été extrait de Shared. Il lit les tables et les énumérations des autres contextes sans posséder leurs règles métier. Les rapports ont ainsi un emplacement défini, mais une modification de table peut toujours casser leurs requêtes SQL. Des tests sur la base couvrent cette dépendance.
L’API possède des bus distincts pour les commandes, les requêtes, les messages différés et les événements. Les commandes et messages des workers s’exécutent normalement dans des transactions Doctrine. Les événements du domaine sont transmis après la validation de la transaction d’origine : une écriture annulée n’annonce donc pas un changement inexistant.
Les réactions aux événements sont exécutées sans garantie de réussite. Si l’une échoue après la validation, l’erreur est journalisée et les autres peuvent continuer ; la commande initiale n’est pas annulée. Cela évite de signaler un échec trompeur pour une modification déjà enregistrée, mais les suites critiques demandent une reprise explicite. Ce mécanisme ne garantit pas que chaque réaction finira par aboutir.
Un middleware d’audit signale les événements enregistrés par un agrégat mais jamais transmis par son gestionnaire. La journalisation des échecs conserve une seule fois le message, le gestionnaire et l’identifiant de corrélation. Les files Redis dédiées aux traitements interactifs, aux notifications et à la télémétrie ont leurs propres paramètres de relance et un transport pour les messages en échec. Ces traces distinguent une action validée de ses suites encore inachevées.
L’importateur MariaDB historique se trouve désormais dans un module supprimable, LegacyImport. Des plans ordonnés traduisent les utilisateurs, le corpus, la facturation, les accès et les données de veille vers le schéma PostgreSQL. Ils utilisent les valeurs et contraintes du modèle cible, travaillent par lots limités et signalent les lignes insérées, ignorées, rejetées ou en échec.
Le parcours opérationnel commence par une capture de l’historique accompagnée de sommes de contrôle et sépare cette capture de l’import. Des reçus d’import et une finalisation reprenable permettent de relancer les opérations, notamment les contrôles de parité de recherche, le transfert des annexes, le rapprochement des citations et la synchronisation des abonnements. Ce parcours est implémenté ; sa présence ne permet pas, à elle seule, de conclure que la migration de production est terminée.
Les responsabilités de déploiement sont également réparties. Le dépôt d’infrastructure prépare les hôtes Ubuntu, Docker, Tailscale et l’enregistrement dans Dokploy. Le dépôt de la plateforme gère les images applicatives, migrations, manifestes de livraison et contrôles de déploiement. pctl fournit la planification des tâches locales et de CI ainsi que les contrats d’environnement. GitHub Actions gère la publication des versions et leur activation par Dokploy.
Le workflow de staging traite les pushs validés sur main. Une version de production identifiée par un tag sémantique exige une validation en staging du même commit. Les manifestes conservent les empreintes exactes des images, et les contrôles de disponibilité comparent le commit de l’API au reçu de déploiement. En développement, Silo fournit désormais le stockage compatible S3 à la place de l’image MinIO précédente.
La suite de tests actuelle dépasse les objets du domaine pris isolément. Les scénarios Behat des contextes métier utilisent désormais les dépôts Doctrine, les contraintes réelles de la base et les middlewares de production du bus de commandes. Les systèmes externes, comme le courriel, le stockage ou les prestataires de paiement, peuvent toujours être remplacés pendant les tests.
Ce changement a révélé des défauts concrets dans Identity : une vérification de sécurité expirée pouvait bloquer la suivante par une contrainte d’unicité, et un verrouillage prévu après cinq échecs de connexion survenait au sixième. L’implémentation retire maintenant la vérification expirée et inclut l’échec courant dans le compteur. Les anciens dépôts en mémoire n’avaient pas révélé ces comportements.
Les tests fonctionnels exercent le rapprochement SQL des alertes, les événements de paiement dupliqués, les requêtes de rapport et les écritures des documents de politique. Vitest couvre les paquets web partagés ; les parcours Playwright utilisent une API, une base et des services isolés. Ces environnements isolés rendent les parcours reproductibles ; le comportement réel des prestataires et la reprise des déploiements demandent encore des contrôles opérationnels.
Les prochaines preuves attendues sont opérationnelles : pertinence de la recherche sur des requêtes juridiques relues, délais d’indexation, échecs de notification, rapprochement des données migrées et reprise des commandes payées en attente de droits. Ces mesures renseigneraient davantage sur le service qu’un décompte de fonctionnalités.