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

Law.Dsl racine + LawDslGenerator — le méta-générateur de cadres juridiques

La Partie 6 a posé la couche technique partagée Common.Dsl : des types-symboles neutres (IAsset, ISpace, IPeriod, IProcedure) sans jugement de valeur. Cette partie monte d'un cran. Law.Dsl est la racine du vocabulaire juridique : elle dit ce qu'est un container légal, une subdivision, une mesure, une procédure démocratique — mais elle ne dit pas comment un container s'appelle dans une tradition donnée (Article dans le droit codifié, Statute en common law, Canon en droit canonique, Edictum en droit romain). Elle est agnostique aux traditions, et c'est exactement ce qui permet au bridge de fonctionner identiquement sur n'importe quel cadre Law.${Space}${Time}.${Author} sans jamais avoir à être réécrit.

Au-dessus de cette racine, Law.Dsl.MetaModel fournit le vocabulaire pour décrire un cadre juridique particulier : quels concepts existent dans ce cadre, quelles spécifications s'appliquent, quels diagnostics doivent être émis. Et LawDslGenerator, le méta-générateur, prend cette description et émet le cadre complet : attributs, analyzers structurels, diagnostics, squelettes de bridge. C'est le cœur de la démocratisation technique du projet : produire un cadre juridique concurrent ne demande pas de maîtriser Roslyn, juste de savoir décrire l'intention du cadre en quelques centaines de lignes.

La racine FrenchExDev.Net.Law.Dsl

La racine est un projet C# minimal, comme Common.Dsl. Pas de Source Generator, pas de dépendance à Roslyn, pas d'I/O. Juste des interfaces et un enum.

namespace FrenchExDev.Net.Law.Dsl;

using FrenchExDev.Net.Common.Dsl;

// Conteneur juridique : Article, Statute, Canon, Edictum...
public interface ILegalContainer
{
    string Identifier { get; }       // "L132-4", "Statute 42", "Canon 17"
    Type? Space { get; }             // doit implémenter ISpace
    Type? Period { get; }            // doit implémenter IPeriod
    string? Codification { get; }    // "Code de l'environnement", "UK Statutes"
}

// Subdivision : Alinéa, Section, Paragraph, Verset...
public interface ILegalSubdivision
{
    int Index { get; }
    string? Text { get; }
}

// Mesure juridique : privatisation, nationalisation, encadrement...
public interface IMeasure
{
    MeasureKind Kind { get; }
    Type TargetAsset { get; }        // doit implémenter IAsset
}

public enum MeasureKind
{
    Privatisation, Nationalisation, Regulation,
    Liberalisation, Suppression, Restoration
}

// Procédure démocratique typée
public interface IDemocraticProcedure
{
    Type ProcedureType { get; }      // doit implémenter IProcedure
    string? ReferenceDate { get; }   // ISO-8601
}

Quatre interfaces, un enum. C'est tout ce que le bridge a besoin de connaître pour analyser n'importe quel cadre juridique. ILegalContainer est le concept le plus général d'un acte juridique structuré — il a un identifiant, un espace géopolitique optionnel, une période temporelle optionnelle, et une codification optionnelle. Qu'il s'appelle Article en droit français codifié ou Statute en common law britannique ne change rien pour le bridge : les deux implémentent ILegalContainer, les deux sont analysables par FrontierCrossingDetector.

IMeasure est la pièce maîtresse du système de détection. Une mesure fait quelque chose à un bien : elle le privatise, le nationalise, l'encadre, le libéralise, le supprime, le restaure. Le MeasureKind est un enum fermé parce que les types de transformations de statut sont finis et universels — ce qui change entre traditions juridiques, c'est le vocabulaire pour les nommer et les conditions pour les valider, pas la nature des transformations elles-mêmes.

IDemocraticProcedure est ce qui relie une mesure à la procédure démocratique qui la légitime. Le ProcedureType est un Type qui doit implémenter IProcedure (défini dans Common.Dsl), ce qui signifie que la procédure porte sa durée minimale légale. Le bridge peut vérifier qu'une procédure a bien été déclarée et que sa durée minimale est respectée.

Notez le pattern Strategy implicite : le bridge Source Generator ne référence que Law.Dsl racine. Il scanne tous les ILegalContainer peu importe leur attribut concret. Une seule version du bridge fonctionne pour Law.France2026.Etalab (codifié), Law.CommonLaw.UK2026.GovUK (common law), Law.RomanLaw.JulienPaul.IUVS (droit romain) ou Law.Canon.Vatican2026.HolySee (droit canonique). C'est exactement le principe ouvert/fermé de Meyer — ouvert à l'extension (nouveaux cadres), fermé à la modification (le bridge ne change jamais).

Law.Dsl.MetaModel — décrire un cadre juridique en tant que cadre

Si Law.Dsl fournit les interfaces que les cadres implémentent, Law.Dsl.MetaModel fournit les attributs que les auteurs utilisent pour décrire leurs cadres. C'est un niveau d'abstraction au-dessus : ce n'est pas du code juridique, c'est du code qui dit quel code juridique va être généré.

namespace FrenchExDev.Net.Law.Dsl.MetaModel;

// Décrit un cadre juridique complet
[AttributeUsage(AttributeTargets.Class)]
public sealed class LegalDslDefinitionAttribute : Attribute
{
    public LegalDslDefinitionAttribute(string name) => Name = name;
    public string Name { get; }
    public string? Description { get; init; }
    public string? Maintainer { get; init; }
    public string BasedOnCommon { get; init; } = "";
    public Type? GeographicScope { get; init; }   // doit implémenter ISpace
    public Type? Time { get; init; }              // doit implémenter IPeriod
    public string LegalTradition { get; init; } = "Codified";
}

// Décrit un concept juridique du cadre
[AttributeUsage(AttributeTargets.Class)]
public sealed class LegalConceptAttribute : Attribute
{
    public LegalConceptAttribute(string name) => Name = name;
    public string Name { get; }
    public string Hierarchy { get; init; } = "Document";
    public Type? ParentConcept { get; init; }
    public AttributeTargets AttributeTargets { get; init; }
    public Type ImplementsInterface { get; init; } = typeof(object);
}

// Décrit une spécification structurelle du cadre
[AttributeUsage(AttributeTargets.Class)]
public sealed class LegalSpecificationAttribute : Attribute
{
    public LegalSpecificationAttribute(string name) => Name = name;
    public string Name { get; }
    public Type AppliesTo { get; init; } = typeof(object);
    public string Severity { get; init; } = "Error";
}

// Décrit un pont bridge déclaré par le cadre
[AttributeUsage(AttributeTargets.Class)]
public sealed class LegalBridgeAttribute : Attribute
{
    public LegalBridgeAttribute(string name) => Name = name;
    public string Name { get; }
    public string Detects { get; init; } = "";
    public Type UsingDetector { get; init; } = typeof(object);
}

LegalDslDefinitionAttribute est le point d'entrée : il dit quel cadre on décrit. Le Name est le FQN du cadre ("Law.France2026.Etalab"), le Maintainer est l'auteur responsable, le BasedOnCommon référence le cadre Common.* dont les symboles sont utilisés, le GeographicScope et Time sont typés via ISpace et IPeriod. Le LegalTradition est une chaîne libre (c'est le seul endroit où une chaîne est acceptable, parce que les traditions juridiques ne forment pas un ensemble fermé — on ne peut pas prétendre énumérer toutes les traditions du monde dans un enum).

LegalConceptAttribute décrit chaque concept du cadre : Article implémente ILegalContainer, Alinea implémente ILegalSubdivision avec ParentConcept = typeof(ArticleConcept), Measure implémente IMeasure, DemocraticProcedure implémente IDemocraticProcedure. Le Hierarchy positionne le concept dans l'arbre du cadre, et les AttributeTargets disent sur quoi l'attribut généré pourra être posé (classe pour un Article, méthode pour un Alinéa).

LegalSpecificationAttribute remplace le mot "contrainte" par le mot spécification — un choix de cadrage fondamental pour ce projet. On ne dit pas « l'alinéa est contraint de vivre dans un article », on dit « le cadre spécifie que l'alinéa se trouve dans un article ». La différence est sémantique et politique : une spécification est constitutive (elle fait partie de la définition du cadre), une contrainte est restrictive (elle s'impose de l'extérieur). Les cadres juridiques du projet sont des cadres spécifiques, pas des cages de contraintes.

LawDslGenerator — le méta-générateur proprement dit

LawDslGenerator est un IIncrementalGenerator Roslyn qui lit les descriptions M2-meta (LegalDslDefinition, LegalConcept, LegalSpecification, LegalBridge) et émet le code complet d'un cadre juridique. Son pipeline est simple dans sa structure :

  1. Il cherche les classes décorées [LegalDslDefinition] dans le projet courant. Chaque classe trouvée est une description de cadre.
  2. Pour chaque définition, il lit les [LegalConcept] et [LegalSpecification] enfants — c'est le contenu de la spécification.
  3. Il émet le cadre complet sous forme de fichiers .g.cs qui vivent dans le projet courant.

Ce que le générateur produit pour chaque cadre décrit :

  • Companions M3 : un companion par LegalConcept, enregistré dans le pipeline M3 du monorepo existant.
  • Attributs : un {Name}Attribute par concept, avec constructeur et propriétés qui implémentent ILegalContainer / ILegalSubdivision / IMeasure / IDemocraticProcedure selon le ImplementsInterface déclaré.
  • Analyzer structurel : un {Author}{Space}{Time}StructuralAnalyzer avec un Guard par LegalSpecification, émettant des diagnostics préfixés LAW_{INSTANCE}_*.
  • Diagnostic descriptors : préfixés par le nom du cadre, donc deux cadres concurrents pour le même (Space, Time) émettent des diagnostics distincts et identifiables.
  • Squelette de bridge : si le cadre déclare un [LegalBridge], un squelette d'analyzer de cohérence est généré, à compléter à la main pour la logique métier propre.
  • Squelette d'IIncrementalGenerator métier : si le cadre a besoin de son propre SG en plus des analyzers génériques, le squelette est là.

90% du squelette d'un cadre est généré. L'auteur n'écrit que trois choses :

  1. La description LegalDslDefinition (~50-100 lignes) — l'intention du cadre.
  2. Les méthodes Validate() des spécifications (logique métier juridique propre au cadre).
  3. La logique du Detector du bridge (la philosophie propre au cadre — ce qu'il considère comme une violation, sachant qu'un autre cadre concurrent peut considérer la même chose comme valide).

C'est en cela que LawDslGenerator démocratise la génération de cadres juridiques. Un nouvel auteur — association, syndicat, université, citoyen — peut générer son propre cadre Law.France2026.MyOrganisation.Dsl en quelques centaines de lignes et le publier en NuGet à côté du cadre Etalab. La barrière d'entrée pour produire un cadre concurrent est technique, faible, et reproductible. Pas besoin de connaître Roslyn, pas besoin de comprendre le pipeline M3, pas besoin d'écrire des analyzers à la main. Juste décrire l'intention, et le méta-générateur fait le reste.

Une instance : Law.France2026.Etalab.Dsl

Voici la description M2-meta d'un cadre pour le droit français codifié contemporain, maintenu par Etalab (DINUM) :

using FrenchExDev.Net.Law.Dsl.MetaModel;
using Common.France2026.Etalab.Spaces;
using Common.France2026.Etalab.Periods;

// Description du cadre — l'intention
[LegalDslDefinition("Law.France2026.Etalab",
    Description = "DSL juridique pour le droit français codifié contemporain",
    Maintainer = "Etalab — DINUM",
    BasedOnCommon = "Common.France2026.Etalab",
    GeographicScope = typeof(FR),
    Time = typeof(Y2026),
    LegalTradition = "Codified")]
public sealed class LawFrance2026EtalabDefinition
{
    // Concept : Article (conteneur principal du droit codifié)
    [LegalConcept("Article",
        Hierarchy = "Document",
        AttributeTargets = AttributeTargets.Class,
        ImplementsInterface = typeof(ILegalContainer))]
    public sealed class ArticleConcept { }

    // Concept : Alinéa (subdivision d'un article)
    [LegalConcept("Alinea",
        Hierarchy = "Subdivision",
        ParentConcept = typeof(ArticleConcept),
        AttributeTargets = AttributeTargets.Method,
        ImplementsInterface = typeof(ILegalSubdivision))]
    public sealed class AlineaConcept { }

    // Concept : Mesure (acte juridique sur un bien)
    [LegalConcept("Measure",
        Hierarchy = "Measure",
        AttributeTargets = AttributeTargets.Method,
        ImplementsInterface = typeof(IMeasure))]
    public sealed class MeasureConcept { }

    // Concept : Procédure démocratique
    [LegalConcept("DemocraticProcedure",
        Hierarchy = "Procedure",
        AttributeTargets = AttributeTargets.Method,
        ImplementsInterface = typeof(IDemocraticProcedure))]
    public sealed class DemocraticProcedureConcept { }

    // Spécification : un Alinea ne peut pas exister hors d'un Article
    [LegalSpecification("AlineaOutsideArticle",
        AppliesTo = typeof(AlineaConcept),
        Severity = "Error")]
    public sealed class AlineaOutsideArticleSpec
    {
        // Logique métier propre — l'auteur l'écrit à la main
        public static SpecificationResult Validate(ConceptValidationContext ctx) { /* ... */ }
    }
}

LawDslGenerator consomme cette description et émet :

  • ArticleAttribute.g.cs : public sealed class ArticleAttribute : Attribute, ILegalContainer { ... }
  • AlineaAttribute.g.cs : pareil pour ILegalSubdivision
  • MeasureAttribute.g.cs : pareil pour IMeasure (paramètre Type targetAsset)
  • DemocraticProcedureAttribute.g.cs : pareil pour IDemocraticProcedure
  • LawFrance2026EtalabStructuralAnalyzer.g.cs : analyzer avec Guards par LegalSpecification
  • LawFrance2026EtalabDiagnostics.g.cs : descriptors LAW_FR2026_ETALAB_*
  • Concepts/*.g.cs : companions M3

L'instance contient en plus, écrits à la main, les valeurs concrètes — par exemple ArticleL132_4.cs qui utilise [Article("L132-4")]. Mais ces valeurs ne sont pas générées : elles viennent du typage humain du droit existant.

Le raccourci tactique du Ship 1

Pour le Ship 1 uniquement (le smoke test cas Dumas), on accepte un raccourci : créer Law.France1995.Erard.Dsl à la main, sans utiliser LawDslGenerator. La description M2-meta est quasi identique à celle de France 2026 — le vocabulaire codifié français est stable sur 31 ans — seul Time = typeof(Y1995) change. LawDslGenerator produirait les mêmes attributs avec un namespace différent (Law.France1995.Erard.Attributes) et un nom de package différent.

L'introduction de LawDslGenerator est repoussée au Ship 5-bis, où on l'utilisera pour régénérer Law.France1995.Erard.Dsl à partir de sa description M2 et vérifier que le résultat est byte-équivalent au code écrit à la main. C'est un test de fidélité du bootstrap — pas une réécriture, un refactor. Le code du Ship 1 reste valide ; il acquiert juste une description M2 qui le génère.

C'est la confirmation pratique de la Partie 4 : sur l'horizon 1995-2026, deux instances Law distinctes mais générées à partir de descriptions équivalentes. La duplication de code est gratuite parce que générée. Chaque cadre vit dans son namespace, porte ses propres diagnostics, et peut évoluer indépendamment sans rompre l'autre.

Pourquoi "cadres spécifiques" et pas "contraintes formelles"

Un point de cadrage qui traverse toute cette partie et toute la série. On ne parle pas de contraintes formelles imposées au droit. On parle de cadres spécifiques générés pour un (Espace, Temps, Auteur) donné. La différence est sémantique, politique, et architecturale.

Sémantiquement : une contrainte s'applique de l'extérieur à un sujet qui la subit. Un cadre est l'espace dans lequel un sujet agit. Chomsky ne dit pas que l'anglais contraint ses locuteurs ; il dit que l'anglais est le cadre spécifique dans lequel ils produisent un nombre infini d'énoncés. Roussel ne dit pas que la contrainte oulipienne restreint l'écrivain ; il dit que le cadre engendre la production.

Politiquement : parler de contraintes positionne le compilateur comme un gendarme qui dit non. Parler de cadres spécifiques le positionne comme un atelier qui dit voici ton espace de travail, avec ses outils et ses spécifications. L'outil ne restreint pas ; il cadre. La distinction fait toute la différence pour l'adoption : un juriste, un citoyen, un législateur entrent plus volontiers dans un cadre que dans un carcan.

Architecturalement : LegalSpecificationAttribute est le nom correct du concept. Pas LegalConstraintAttribute. Le code porte la philosophie.

Pour aller plus loin

⬇ Download