Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Les six régimes — tests, simulations, citoyen-auteur

Dès qu'on type le droit comme du code, il devient mou au sens du génie logiciel : manipulable, testable, simulable, réfutable, forkable, replayable, comparable. Le droit non-typé est un texte figé dans un Journal Officiel ; le droit typé est un artefact compilable qui supporte toute la panoplie d'outils que le génie logiciel a inventée en soixante ans pour vérifier, transformer, comparer et distribuer du code. Six régimes outillent cette malléabilité nouvelle. Les cinq premiers outillent les producteurs de loi — législateurs, juristes, journalistes, militants, chercheurs. Le sixième outille les destinataires — les gens. C'est le plus émancipateur, et c'est celui qui justifie l'existence de toute l'infrastructure.

Régime 1 — Tests unitaires purs sur FrontierCrossingDetector

Le premier régime est le plus simple et le plus rapide. Il teste le FrontierCrossingDetector — la fonction pure qui prend un CrossModel en entrée et retourne une liste de CrossingViolation — sans jamais toucher à Roslyn, sans compiler de fichier .cs, sans charger d'assembly. Le modèle est construit en mémoire, le détecteur l'analyse, on vérifie le résultat.

public class FrontierCrossingDetectorTests
{
    [Fact]
    public void Privatization_of_Commons_without_Referendum_emits_LOI003()
    {
        // Arrange — un modèle minimal : un article avec une mesure de privatisation
        // sur un bien classé Commons, sans procédure démocratique déclarée
        var model = new CrossModel(
            Articles: new[] {
                new LawArticleModel(
                    new ArticleFqn("MyCorpus.WaterPrivatizationAct"), "L100",
                    Space: new SpaceFqn("global::Common.France2026.Etalab.Spaces.FR"),
                    Period: new PeriodFqn("global::Common.France2026.Etalab.Periods.Y2026"),
                    Alineas: new[] {
                        new LawAlineaModel(1,
                            new AlineaFqn("MyCorpus.WaterPrivatizationAct.Article1"),
                            Measures: new[] {
                                new LawMeasureModel(MeasureKindCode.Privatization,
                                    new AssetFqn("global::Common.France2026.Etalab.Assets.DrinkingWater"))
                            },
                            Procedures: Array.Empty<LawProcedureModel>())
                    })
            },
            Assets: new[] {
                new CommonsAssetStatusModel(
                    new AssetFqn("global::Common.France2026.Etalab.Assets.DrinkingWater"),
                    new[] { new CommonsStatusModel(StatusKindCode.Commons,
                        new SpaceFqn("global::Common.France2026.Etalab.Spaces.FR"),
                        new PeriodFqn("global::Common.France2026.Etalab.Periods.Y2026")) })
            },
            Boundaries: new[] {
                new CommonsBoundaryModel(StatusKindCode.Commons, StatusKindCode.Private,
                    new ProcedureFqn("global::Common.France2026.Etalab.Procedures.NationalReferendum"))
            });

        // Act
        var violations = FrontierCrossingDetector.Analyze(model);

        // Assert — exactement une violation, sur le bon article, le bon alinéa
        violations.Should().ContainSingle(v =>
            v.Code == ViolationCode.LOI003_MissingProcedure &&
            v.Article.Value.Contains("WaterPrivatizationAct") &&
            v.Alinea == 1);
    }

    [Fact] public void Privatization_with_Referendum_passes() { /* ... */ }
    [Fact] public void Measure_on_unknown_Asset_emits_LOI001() { /* ... */ }
    [Fact] public void Status_with_wrong_Space_emits_LOI004() { /* ... */ }
    [Fact] public void Wrong_procedure_type_emits_LOI005() { /* ... */ }
    [Fact] public void Self_targeting_measure_does_not_emit() { /* ... */ }
    [Fact] public void Restoration_without_procedure_emits_only_info() { /* ... */ }
    [Fact] public void Period_outside_known_statuses_emits_LOI008() { /* ... */ }
    [Fact] public void Multiple_statuses_picks_most_recent_for_current() { /* ... */ }
}

Ce sont environ douze à quinze tests purs, sans dépendance Roslyn, qui courent en moins de cent millisecondes. Ils constituent le premier filet de sécurité du projet — si l'un d'eux se casse, le détecteur a un bug. Le pattern est celui du QualityGate existant (cf. Partie 11) : les fakes sont écrits à la main, les modèles sont construits en mémoire, la rapidité d'exécution est un objectif de conception.

Régime 2 — Tests d'intégration end-to-end sur le cas Dumas

Le deuxième régime est le smoke test du projet. Il compile une vraie source DecisionDumas.cs via RoslynLawTestHelper.CreateProject(), exécute le bridge SG dans le pipeline Roslyn, et vérifie que le diagnostic LOI003 apparaît au bon emplacement avec le bon message.

C'est le test qui prouve que le système fonctionne sur un cas historique réel — celui décrit en détail dans la Partie 10 — Trace d'exécution Dumas. Le test doit échouer quand DecisionDumas.Section1() n'a pas de [DemocraticProcedure], et doit réussir quand on l'ajoute. C'est la version automatisée de la trace manuelle des étapes A à H.

L'intérêt du régime 2 par rapport au régime 1 est qu'il teste l'intégration complète : l'extraction du CrossModel depuis l'arbre syntaxique Roslyn, la résolution des FQN, la construction du LocationIndex, la transformation de la violation en Diagnostic, l'attachement de la Location au bon noeud syntaxique. Le régime 1 teste le détecteur isolé ; le régime 2 teste le pipeline complet. Les deux sont nécessaires.

Régime 3 — Simulations what-if avec ScenarioRunner

Le troisième régime est celui qui transforme le compilateur en instrument de prospective politique. Le ScenarioRunner prend un modèle de base (le corpus juridique actuel) et un scénario (un ensemble de modifications hypothétiques), applique le scénario au modèle, et compare les violations avant et après.

// Un scénario est un delta typé sur le CrossModel
public sealed record PolicyScenario(
    string Name,
    string Description,
    IReadOnlyList<CommonsAssetStatusModel> AddedAssets,
    IReadOnlyList<CommonsAssetStatusModel> ReplacedAssets,
    IReadOnlyList<CommonsBoundaryModel> AddedBoundaries,
    IReadOnlyList<LawArticleModel> AddedArticles);

// Le résultat est un diff typé entre avant et après
public sealed record SimulationOutcome(
    PolicyScenario Scenario,
    int ViolationsBefore,
    int ViolationsAfter,
    IReadOnlyList<CrossingViolation> NewViolations,
    IReadOnlyList<CrossingViolation> ResolvedViolations,
    IReadOnlyList<ArticleFqn> NowFailingArticles,
    IReadOnlyList<ArticleFqn> NowPassingArticles);

public static class ScenarioRunner
{
    // Applique le scénario et compare — fonction pure, déterministe
    public static SimulationOutcome Run(PolicyScenario scenario, CrossModel baseline)
    {
        var before = FrontierCrossingDetector.Analyze(baseline);
        var afterModel = ApplyDelta(baseline, scenario);
        var after = FrontierCrossingDetector.Analyze(afterModel);

        return new SimulationOutcome(
            Scenario: scenario,
            ViolationsBefore: before.Count,
            ViolationsAfter: after.Count,
            NewViolations: after.Where(a => !before.Contains(a)).ToList(),
            ResolvedViolations: before.Where(b => !after.Contains(b)).ToList(),
            NowFailingArticles: /* diff des articles en erreur */,
            NowPassingArticles: /* diff des articles résolus */);
    }
}

L'exemple canonique est : « et si on classait l'eau potable comme commun en France 2030 ? » On construit un PolicyScenario qui ajoute un CommonsAssetStatusModel avec StatusKind.Commons pour DrinkingWater dans l'espace FR et la période Y2030, et qui ajoute une CommonsBoundaryModel exigeant un NationalReferendum pour la transition Commons → Private. On passe ce scénario au ScenarioRunner avec le corpus Law.France2030.Etalab comme baseline. Le résultat, en deux cents millisecondes : « les articles L132-4, L132-5, L132-6 deviennent des compile errors parce qu'ils contiennent des privatisations sans référendum préalable ».

C'est l'effet politique simulé de la reclassification, mesurable, déterministe, reproductible. La CLI est un one-liner :

dotnet law-bridge simulate \
  --baseline ./Common.France2030.Etalab.dll \
  --scenario ./Scenarios/DrinkingWaterReclassification.json \
  --report ./out/simulation-report.html

C'est un changement de régime cognitif. Avant cette infrastructure, l'effet d'une loi sur le corpus existant est un débat parlementaire lent et opaque, où les parlementaires votent des textes dont les effets croisés ne sont mesurés que par les services juridiques des ministères, et encore, partiellement. Après, c'est dotnet law-bridge simulate, en deux cents millisecondes, déterministe, reproductible, partageable, forkable, versionné dans Git, diffable d'une simulation à l'autre. Le ScenarioRunner ne remplace pas le débat parlementaire — il l'arme d'un outil de visualisation que tout le monde peut utiliser, pas seulement les techniciens du Conseil d'État.

Régime 4 — Property-based testing avec FsCheck

Le quatrième régime est le plus puissant épistémologiquement. Au lieu de tester des cas particuliers (régime 1) ou un cas historique (régime 2) ou des scénarios hypothétiques (régime 3), il teste des propriétés universelles du détecteur sur des milliers de modèles générés aléatoirement par FsCheck.

// Propriété 1 — Déterminisme : deux exécutions sur le même modèle
// retournent toujours le même résultat
[Property]
public Property Detector_is_deterministic(CrossModel model)
{
    var run1 = FrontierCrossingDetector.Analyze(model);
    var run2 = FrontierCrossingDetector.Analyze(model);
    return run1.SequenceEqual(run2).ToProperty();
}

// Propriété 2 — Monotonicité : ajouter une procédure ne crée jamais
// de nouvelle violation (intuition juridique : plus de démocratie = moins de problèmes)
[Property]
public Property Adding_a_procedure_never_creates_new_violations(
    CrossModel model, LawProcedureModel newProcedure, int alineaIndex)
{
    var modified = model.WithAddedProcedure(alineaIndex, newProcedure);
    var before = FrontierCrossingDetector.Analyze(model);
    var after = FrontierCrossingDetector.Analyze(modified);
    return (after.Count <= before.Count).ToProperty();
}

// Propriété 3 — Conservation : retirer un bien retire toutes ses violations
// (pas de violation orpheline qui persisterait après suppression de sa cause)
[Property]
public Property Removing_an_asset_always_removes_its_violations(
    CrossModel model, AssetFqn assetToRemove)
{
    var modified = model.WithoutAsset(assetToRemove);
    var afterViolations = FrontierCrossingDetector.Analyze(modified);
    return afterViolations.All(v => !v.Message.Contains(assetToRemove.Value)).ToProperty();
}

// Propriété 4 — L'invariant central : la privatisation d'un commun
// sans référendum est *toujours* une violation, quel que soit le contexte
[Property]
public Property Privatization_of_Commons_without_Referendum_always_fails(
    AssetFqn asset, SpaceFqn space, PeriodFqn period, ProcedureFqn referendum)
{
    /* construit un modèle minimal avec le bien classé Commons,
       une frontière Commons → Private exigeant le referendum,
       et un article avec Privatization sans procédure → LOI003 toujours */
}

Ces quatre propriétés sont des théorèmes sur le comportement du détecteur, vérifiés sur des milliers de modèles générés aléatoirement. Le déterminisme garantit que le compilateur ne change pas d'avis selon l'heure du jour ou l'ordre d'évaluation. La monotonicité garantit qu'ajouter de la démocratie ne crée jamais de nouveaux problèmes — c'est une intuition juridique profonde, et si elle se brisait un jour, ce serait le signe qu'on a introduit un bug ou qu'on a changé une définition fondamentale. La conservation garantit qu'il n'y a pas de violations orphelines — si un bien n'existe plus, ses violations n'existent plus non plus. L'invariant central garantit que le cas de base du projet (privatisation d'un commun sans procédure démocratique) est toujours détecté, quels que soient les paramètres aléatoires du contexte.

Si l'une de ces propriétés se brise un jour, c'est qu'on a introduit un bug ou qu'on a changé une définition fondamentale du système. Dans les deux cas, on veut le savoir immédiatement, pas dans six mois quand un juriste se rend compte que le compilateur a changé d'avis. Le property-based testing est le filet de sécurité le plus rigoureux du projet.

Régime 5 — Comparaison multi-sources avec ConsensusScorer

Le cinquième régime est celui qui rend la pluralité des cadres opérationnelle. Si plusieurs sources ont typé différemment la même situation — La Quadrature classe l'eau comme Commons, un think-tank libéral la classe comme Private, Etalab la classe comme Public — quel est le verdict pour la même loi dans chaque cadre ?

// Le verdict d'une source sur un article donné
public sealed record SourceVerdict(
    string SourceName,
    int ViolationsCount,
    bool LawCompiles,
    IReadOnlyList<CrossingViolation> Violations);

// Le rapport de consensus agrège les verdicts
public sealed record ConsensusReport(
    LawArticleModel Article,
    IReadOnlyList<SourceVerdict> Verdicts,
    double ConsensusScore,
    string DivergenceSummary);

public static class ConsensusScorer
{
    public static ConsensusReport Compare(
        LawArticleModel article,
        IReadOnlyDictionary<string, CrossModel> sources)
    {
        // Analyse l'article dans chaque cadre source
        var verdicts = sources.Select(kvp =>
        {
            var violations = FrontierCrossingDetector.Analyze(
                kvp.Value.WithSingleArticle(article));
            return new SourceVerdict(kvp.Key, violations.Count,
                violations.Count == 0, violations);
        }).ToList();

        // Consensus = proportion maximale d'accord
        var compileCount = verdicts.Count(v => v.LawCompiles);
        var consensus = (double)Math.Max(compileCount,
            verdicts.Count - compileCount) / verdicts.Count;

        return new ConsensusReport(article, verdicts, consensus,
            BuildDivergenceSummary(verdicts));
    }
}

La CLI prend l'article à analyser et les sources à comparer :

dotnet law-bridge consensus \
  --article ProjetLoiEau2030.cs \
  --source LaQuadrature=./LaQuadrature.dll \
  --source Etalab=./Etalab.dll \
  --source InstitutMontaigne=./InstitutMontaigne.dll

Sortie : « Article ProjetLoiEau2030 — Consensus 0.33. LaQuadrature : NE COMPILE PAS (2 violations). Etalab : NE COMPILE PAS (1 violation). InstitutMontaigne : COMPILE (0 violation). Divergence : statut typé de l'eau potable. Implication politique : la loi proposée ne fait consensus que sur 33% des sources sérieuses. »

C'est le moment où la métacratie devient un outil de débat public. Le désaccord est explicite, typé, traçable. Le ConsensusScorer ne dit pas qui a raison — il dit où est le désaccord et sur quoi il porte. C'est au débat parlementaire, au débat public, au juge, à la presse de trancher. Mais le matériau du débat est maintenant structuré, navigable, diffable. Personne ne peut prétendre que « tout le monde sait » ou que « la question ne se pose pas ». Le ConsensusScore de 0.33 est un fait typé, pas une opinion.

C'est aussi la preuve pratique de ce qui est affirmé depuis la Partie 1 : le compile error n'est pas une vérité absolue — c'est le verdict d'un cadre spécifique. Le ConsensusScorer rend cette multiplicité visible et mesurable.

Régime 6 — Le citoyen comme auteur

Les régimes 1 à 5 outillent les producteurs de loi. Ils restent dans le registre expert, même si les outils sont ouverts et que n'importe qui peut les utiliser. Le sixième régime change de registre. Il outille les destinataires du droit — les gens — en leur permettant d'écrire leur propre cas dans un fichier C#, de lancer dotnet build localement, et de recevoir du compilateur un guide typé vers leurs droits.

L'exemple de Mathilde Martin

Mathilde est une locataire de logement social, parent isolé, avec deux enfants à charge, et un revenu inférieur au seuil de pauvreté. Elle veut savoir si son bailleur peut augmenter son loyer, quelles aides elle peut activer après une perte d'emploi, et quel recours elle a si le loyer est contestable. Elle crée un fichier MyCases/MyCase.cs :

using Citizen.France2026.Etalab.Attributes;
using Citizen.France2026.Etalab.Circumstances;
using Citizen.France2026.Etalab.Questions;

namespace MyCases.MathildeMartin;

// Mes circonstances — le compilateur les utilise pour filtrer le droit applicable
[Circumstance(typeof(SocialHousingTenant))]
[Circumstance(typeof(SingleParent))]
[Circumstance(typeof(DependentChildren), Count = 2)]
[Circumstance(typeof(IncomeBelowPovertyThreshold))]
public sealed class MyCircumstances { }

// Mes questions — le compilateur produit un diagnostic CIT par question
[Question(typeof(CanMyLandlordRaiseMyRent))]
public void RentQuestion() { }

[Question(typeof(AidsAfterJobLoss))]
public void AidsQuestion() { }

[Question(typeof(RentContestationRecourse))]
public void RecourseQuestion() { }

Quand Mathilde lance dotnet build, le Citizen.Bridge.SourceGenerator analyse ses circonstances, croise chaque question avec le corpus juridique Law.France2026.Etalab, et produit des diagnostics Roslyn typés :

MyCases/MyCase.cs(20,17): info CIT001: Question "CanMyLandlordRaiseMyRent"
  → Articles applicables : L353-9-2, L442-1, L442-3
  → Réponse typée : OUI dans la limite de l'IRL ; NON si le logement présente un DPE F ou G
  → Procédure de contestation : ContestationDevantCDC, gratuite, délai 2 mois

MyCases/MyCase.cs(23,17): info CIT002: Question "AidsAfterJobLoss"
  → Vos circonstances [SingleParent + DependentChildren + IncomeBelowPovertyThreshold] activent :
     - RSAMajored (parent isolé)
     - ReinforcedAPL
     - ActivityBonus
  → Procédure d'activation : CAFOnlineProcess, délai 5 jours ouvrés
  → Article(s) référence : L262-9 CASF

MyCases/MyCase.cs(26,17): warn CIT010: Question "RentContestationRecourse"
  → Recours typé disponible : ContestationDevantCDC
  → ATTENTION : votre [Circumstance(SocialHousingTenant)] active une procédure différente
    de droit privé : AllocationCommitteeReferral avant la CDC
  → Délai à respecter : 30 jours après notification

Ce ne sont pas des hallucinations LLM. Ce sont des diagnostics Roslyn typés, qui pointent vers des types Article réels. Mathilde peut faire F12 sur L353-9-2 et lire le texte complet de l'article. Elle peut faire Find All References sur RSAMajored et voir toutes les conditions d'activation dans le cadre. Elle peut écrire un test unitaire sur sa situation et vérifier que le résultat est stable d'une version du cadre à l'autre. Et c'est local par design : aucune donnée envoyée à un serveur, aucun tracking, aucun chatbot, aucun compte à créer.

Les CodeFixers émancipateurs

Les CodeFixers de Law.Citizen.Bridge.Analyzers ne corrigent pas du code — ils proposent des actions juridiques. C'est la transposition la plus radicale du projet : le Quick Fix de l'IDE, qui d'habitude propose de renommer une variable ou d'ajouter un using, propose ici d'activer un droit social ou de lancer une procédure de contestation.

Le ActivateRightCodeFix ajoute [Activate(typeof(RSAMajored))] à la propriété et génère un fichier MySteps.g.md avec les étapes de la démarche. Le LaunchProcedureCodeFix ajoute [Procedure(typeof(ContestationDevantCDC), LaunchDate = "2026-04-15")] et génère un formulaire pré-rempli à partir des circonstances déclarées. Le SimulateHypothesisCodeFix crée un fichier Simulation.IfJobLoss.cs qui clone le cas avec un changement et appelle le ScenarioRunner du régime 3 pour montrer l'effet de la modification.

L'IDE devient un panneau de bord juridique personnel, où chaque ampoule rouge correspond à un droit non-activé et chaque Quick Fix l'active. Ce n'est pas de la science-fiction technique — les CodeFixers sont un mécanisme Roslyn standard utilisé par des dizaines de packages NuGet. Ce qui est nouveau, c'est que le code fixé n'est pas du code C# au sens habituel — c'est une description typée d'une situation de vie.

Property-based testing sur les cas

Le property-based testing du régime 4 s'applique aussi aux cas citoyens :

// Si on ajoute un enfant à charge, les aides totales ne diminuent jamais
// Si cette propriété échoue sur un corpus juridique réel,
// on a trouvé un bug du droit français (effet de seuil)
[Property]
public Property Adding_a_child_never_reduces_total_aid(MyCase baseCase)
{
    var modified = baseCase with { DependentChildrenCount = baseCase.DependentChildrenCount + 1 };
    var aidsBefore = CaseLawMatcher.ComputeTotalAids(baseCase);
    var aidsAfter = CaseLawMatcher.ComputeTotalAids(modified);
    return (aidsAfter >= aidsBefore).ToProperty();
}

Si cette propriété échoue un jour sur un corpus juridique réel, on a trouvé un bug du droit français : une situation où avoir un enfant supplémentaire réduit les aides totales à cause d'un effet de seuil. C'est un appel à amender la loi, et c'est trouvable par le compilateur. Le property-based testing ne détecte pas seulement les bugs du détecteur (régime 4) — il détecte les bugs du droit lui-même, à condition que le typage du corpus soit fidèle.

Pourquoi ce régime change tout

Les régimes 1 à 5 outillent les producteurs de loi. Ils restent dans le registre de l'expertise, même ouverte. Le régime 6 outille les destinataires : les gens. Sans intermédiaire, sans tiers de confiance, sans frais. C'est la réponse pratique à la question méta-sociale « qui écrit ? » (cf. Partie 16) : les gens écrivent leur propre cas, et le compilateur écrit la jurisprudence applicable.

Et c'est substantiellement différent d'un chatbot juridique. Les diagnostics sont typés, réfutables, navigables, déterministes, locaux. Là où un LLM dit « vous avez probablement droit à... » avec un taux d'hallucination non nul et une traçabilité nulle, le compilateur dit CIT001: vous activez RSAMajored via l'article L262-9 — F12 pour le texte. Pas de prompt à écrire, pas de session à recommencer, pas de température à ajuster, pas de contexte perdu après 4096 tokens. Le diagnostic est un fait typé, pas une probabilité linguistique.

L'architecture de Law.Citizen.Bridge est identique à celle de Law.Commons.Bridge (cf. Partie 9) : cinq sous-projets (Core, Roslyn, SourceGenerator, Analyzers, Cli) partageant le même Bridge.Core (Identifiers.cs, pattern Guard, LocationIndex). Seules diffèrent les classes du modèle (CitizenCaseModel, CaseLawMatcher, Recommendation) et le détecteur (CaseLawMatcher.Match). Le fait que la même architecture en cinq sous-projets serve à la fois le bridge inter-cadres experts (régimes 1-5) et le bridge citoyen (régime 6) est une preuve structurelle que le projet ne réserve pas ses outils à une élite : le citoyen utilise la même infrastructure que le juriste, avec un détecteur différent et des CodeFixers différents.

Les six régimes comme écosystème

Les six régimes ne sont pas indépendants. Le régime 1 (tests unitaires) alimente les métriques du QualityGate (cf. Partie 11). Le régime 2 (intégration Dumas) est le smoke test permanent qui prouve que le pipeline fonctionne de bout en bout (cf. Partie 10). Le régime 3 (simulations) utilise le même FrontierCrossingDetector que les régimes 1 et 2 mais sur des modèles hypothétiques. Le régime 4 (property-based) est le filet de sécurité ultime qui vérifie que les régimes 1-3 ne manquent pas de cas pathologiques. Le régime 5 (consensus) agrège les résultats de plusieurs cadres pour rendre la pluralité mesurable. Le régime 6 (citoyen) utilise toute l'infrastructure construite par les cinq autres pour la mettre entre les mains des destinataires du droit.

Ensemble, les six régimes transforment le droit typé d'un artefact statique en un outil vivant, manipulable par tous les acteurs concernés, chacun à son niveau de compétence et à son échelle d'ambition. Le législateur utilise le régime 3 pour simuler l'impact d'un projet de loi. Le juriste utilise le régime 2 pour vérifier un cas historique. Le chercheur en sciences sociales utilise le régime 4 pour trouver des propriétés structurelles du droit. Le journaliste utilise le régime 5 pour mesurer le consensus entre cadres concurrents. Et Mathilde utilise le régime 6 pour savoir si elle a droit au RSA majoré. C'est le même compilateur dans les six cas. La différence est dans le qui et le pourquoi, pas dans le comment.


Partie précédente : Partie 11 — Law.QualityGate | Partie suivante : Partie 13 — Versioning et packaging NuGet | Retour au sommaire

⬇ Download