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

Common.Dsl racine + CommonDslGenerator + instances de cadres

Common.Dsl est la racine d'un méta-générateur de cadres techniques. Elle ne contient aucun cadre concret — elle définit le vocabulaire d'interfaces que tous les cadres Common.${Space}${Time}.${Author} viendront implémenter avec leurs spécifications propres. À la racine, on a les types des choses qui peuvent être typées (un actif, un espace, une période, une procédure, un statut) ; dans chaque cadre généré, on a les choses spécifiques typées (l'eau potable de la France 1995 vue par Erard, les communes de France 2026 vues par Etalab, etc.). La racine est stable et générique ; les cadres sont volatils, multiples, et spécifiques.

La racine FrenchExDev.Net.Common.Dsl

La racine est un projet C# minimal. Pas de Source Generator. Pas de dépendance à Roslyn. Pas d'I/O. Pas même de dépendance NuGet externe. Juste des interfaces et quelques value types.

namespace FrenchExDev.Net.Common.Dsl;

// Marker interface : tout type pivot "actif juridique" implémente ceci
public interface IAsset { }

// Système de typage géographique avec hiérarchie
public interface ISpace { }
public interface ISubdivisionOf<TParent> : ISpace where TParent : ISpace { }

// Système de typage temporel
public interface IPeriod {
    int From { get; }
    int To { get; }   // == From pour une année unique
}

// Marker pour les procédures démocratiques typées
public interface IProcedure {
    string Name { get; }
    int MinDurationDays { get; }
}

// Marker pour les statuts (l'enum reste utile pour Boundary.From/To)
public interface IStatus { StatusKind Kind { get; } }

public enum StatusKind { Commons, Public, Private, Contested, InTransition }

S'ajoute une struct YearOrYearPeriod qui implémente IPeriod avec les méthodes Equals, Contains, Overlaps, Range. C'est le type concret par défaut pour exprimer des périodes temporelles dans les instances.

Notez l'absence totale de jugement de valeur dans cette racine. IAsset ne dit pas si un actif est bon ou mauvais. ISpace ne dit pas si un territoire est légitime ou non. IPeriod ne dit pas si une époque était heureuse ou tragique. IProcedure ne dit pas si une procédure est démocratique ou autoritaire — elle dit juste qu'une procédure a un nom et une durée minimale. StatusKind énumère les cinq statuts possibles d'un actif sans privilégier aucun. C'est uniquement du vocabulaire de référence neutre. Les jugements politiques arrivent au niveau supérieur, dans Commons.Dsl (cf. Partie 8).

Les attributs de description, consommés par CommonDslGenerator

Pour qu'un auteur puisse produire un cadre Common.${Space}${Time}.${Author}.Dsl, il décrit ses actifs, ses espaces, ses périodes et ses procédures à l'aide d'attributs C# que CommonDslGenerator viendra ensuite consommer pour générer les implémentations des interfaces.

namespace FrenchExDev.Net.Common.Dsl.Attributes;

[AttributeUsage(AttributeTargets.Assembly)]
public sealed class SymbolSourceAttribute : Attribute {
    public SymbolSourceAttribute(string instanceName) => InstanceName = instanceName;
    public string InstanceName { get; }   // "France2026.Etalab"
}

[MetaConcept(typeof(SpaceConcept))]
[AttributeUsage(AttributeTargets.Class)]
public sealed class SpaceAttribute : Attribute {
    public SpaceAttribute(string canonicalName) => CanonicalName = canonicalName;
    public string CanonicalName { get; }
    public Type? Parent { get; init; }
}

[MetaConcept(typeof(PeriodConcept))]
[AttributeUsage(AttributeTargets.Class)]
public sealed class PeriodAttribute : Attribute {
    public PeriodAttribute(int year) { From = year; To = year; }
    public PeriodAttribute(int from, int to) { From = from; To = to; }
    public int From { get; }
    public int To { get; }
}

[MetaConcept(typeof(AssetConcept))]
[AttributeUsage(AttributeTargets.Class)]
public sealed class AssetAttribute : Attribute {
    public AssetAttribute(string label) => Label = label;
    public string Label { get; }                // libellé humain — peut être en français
    public string? Category { get; init; }
}

[MetaConcept(typeof(ProcedureConcept))]
[AttributeUsage(AttributeTargets.Class)]
public sealed class ProcedureAttribute : Attribute {
    public ProcedureAttribute(string name, int minDurationDays) {
        Name = name; MinDurationDays = minDurationDays;
    }
    public string Name { get; }
    public int MinDurationDays { get; }
}

Notez deux choses. D'abord, chaque attribut de description est lié à son MetaConcept companion via l'attribut M3 [MetaConcept(typeof(...))] — c'est le pattern de fixed point chomskyen sowaien évoqué en Partie 3, qui rend la racine self-describing. Ensuite, les libellés (les chaînes de caractères passées aux constructeurs) restent dans la langue d'origine du contenu juridique — typiquement en français pour un cadre français. Les identifiants (les noms de classes, d'interfaces, d'attributs, de propriétés) sont en anglais. Cette distinction identifiers/data strings est centrale et a été formalisée en Partie 2.

Une instance complète : Common.France1995.Erard.Dsl

Voici à quoi ressemble une instance complète de cadre technique. Cette instance sert de support au smoke test cas Dumas du Ship 1, qui sera tracé end-to-end en Partie 10. Elle est volontairement minimale — juste assez pour reproduire le cas Dumas — mais elle illustre la structure complète qu'aurait un cadre plus ambitieux.

using FrenchExDev.Net.Common.Dsl.Attributes;

[assembly: SymbolSource("France1995.Erard")]

namespace Common.France1995.Erard.Spaces;

[Space("Monde")]                                       public sealed partial class World { }
[Space("Union européenne", Parent = typeof(World))]    public sealed partial class EU { }
[Space("France",            Parent = typeof(EU))]      public sealed partial class FR { }

namespace Common.France1995.Erard.Periods;

[Period(1995)]            public sealed partial class Y1995 { }
[Period(1988, 1995)]      public sealed partial class P1988_1995 { }

namespace Common.France1995.Erard.Assets;

[Asset("Financement public des campagnes électorales", Category = "Vie démocratique")]
public sealed partial class PublicCampaignFunding { }

[Asset("Plafond des dépenses électorales", Category = "Vie démocratique")]
public sealed partial class ElectoralSpendingCap { }

namespace Common.France1995.Erard.Procedures;

[Procedure("Débat parlementaire (2 lectures)", minDurationDays: 30)]
public sealed partial class ParliamentaryDebateTwoReadings { }

[Procedure("Référendum national",              minDurationDays: 90)]
public sealed partial class NationalReferendum { }

Quelques observations sur cette instance.

Le [assembly: SymbolSource("France1995.Erard")] au sommet du fichier est ce qui identifie l'instance comme un tout. Il dit « cet assembly est un cadre Common pour France 1995, signé par Erard ». C'est ce marqueur que CommonDslGenerator cherche pour savoir qu'il doit traiter ce projet comme une instance de cadre, et pas comme un projet C# ordinaire.

Les espaces sont organisés en hiérarchie via le Parent. FR est subdivision de EU, qui est subdivision de World. Cette hiérarchie est typée (le Parent = typeof(World) est vérifié au compile-time — si on essayait de mettre typeof(string), le compilateur protesterait), et elle sera reflétée dans le code généré sous forme de partial class FR : ISubdivisionOf<EU>.

Les périodes sont des classes scellées partielles, parce que CommonDslGenerator viendra leur ajouter l'implémentation IPeriod avec les properties From, To, Range. L'auteur ne les écrit pas à la main — il déclare juste le [Period(1995)] et le générateur fait le reste.

Les actifs sont les choses qui peuvent être manipulées par les autres cadres. PublicCampaignFunding est un actif typé, qui sera référencé via typeof(...) par les cadres Law (quand une mesure cible cet actif), Commons (quand un statut est attaché à cet actif), Citizen (quand une question citoyenne porte sur cet actif). Chaque référence cross-cadres est typée, jamais une chaîne magique.

Les procédures portent leur durée minimale légale. ParliamentaryDebateTwoReadings requiert 30 jours minimum, NationalReferendum requiert 90 jours minimum. Ces durées sont des données que les détecteurs de bridge peuvent utiliser pour vérifier qu'une procédure respecte son délai.

Ce que produit CommonDslGenerator

CommonDslGenerator lit ces classes décorées et émet Common.France1995.Erard.Generated/Symbols.g.cs avec :

  • Les implémentations des interfaces : partial class World : ISpace, partial class FR : ISubdivisionOf<EU>, partial class Y1995 : IPeriod, partial class PublicCampaignFunding : IAsset, partial class ParliamentaryDebateTwoReadings : IProcedure, etc.
  • Les properties calculées : Name, MinDurationDays, From, To — autant que d'attributs renseignés dans les déclarations
  • La hiérarchie ISubdivisionOf<TParent> quand Parent est défini sur un [Space]
  • Un France1995ErardRegistry global statique avec listes par catégorie : France1995ErardRegistry.Assets, France1995ErardRegistry.Spaces, France1995ErardRegistry.Periods, France1995ErardRegistry.Procedures, et des sous-listes par Category quand applicable

Le registre statique est ce qui permet aux autres cadres de parcourir le contenu d'un cadre Common référencé sans avoir à scanner les assemblies eux-mêmes. Quand Law.Commons.Bridge veut savoir quels actifs existent dans le Common.France1995.Erard qu'il a en référence, il appelle France1995ErardRegistry.Assets et obtient une IReadOnlyList<IAsset> typée.

Pourquoi aucune string magique

Une règle fondamentale du projet : aucune chaîne de caractères n'est jamais utilisée comme pivot par les autres cadres. Les autres DSLs référencent toujours typeof(Common.France1995.Erard.Assets.PublicCampaignFunding), jamais "PublicCampaignFunding" ou "public-campaign-funding".

Pourquoi ? Parce que le type est cadre-aware alors que la chaîne est orpheline. Le type Common.France1995.Erard.Assets.PublicCampaignFunding porte dans son namespace l'identité complète du cadre d'origine — il dit « je suis l'actif PublicCampaignFunding tel que défini dans le cadre Common.France1995.Erard ». Si un autre cadre Common.France2026.Etalab définit aussi un actif appelé PublicCampaignFunding, c'est un autre type C#Common.France2026.Etalab.Assets.PublicCampaignFunding — et le compilateur ne les confondra jamais. Une chaîne magique "PublicCampaignFunding", elle, serait orpheline : aucun moyen de savoir de quel cadre elle vient, aucun moyen de naviguer à F12 vers sa définition, aucune protection compile-time contre les fautes de frappe.

Ce principe est documenté dans la mémoire utilisateur sous le nom « English identifiers in English posts » et il est plus fondamental encore que la convention de nommage : c'est l'idée que toute chose dans un système typé est un type, jamais une chaîne — sauf les URLs et les libellés humains affichés à l'écran. Les références cross-cadres sont des choses, donc des types. Les libellés affichés à Mathilde sont des données humaines, donc des chaînes.

⬇ Download