Law.Commons.Bridge — le pont entre deux familles de cadres
Law.Commons.Bridge n'est pas un compilateur monolithique qui prétendrait unifier le droit et la politique. C'est un pont entre deux familles de cadres concurrents : la famille Law.${Space}${Time}.${Author} (les cadres juridiques, posés en Partie 7) et la famille Commons.${Space}${Time}.${Author} (les cadres politiques sur les biens, posés en Partie 8). Le bridge ne fait qu'une chose : vérifier que les deux cadres choisis par l'auteur d'un cas sont mutuellement cohérents.
Si l'auteur référence Law.France1995.Erard et Commons.France1995.Erard, le bridge vérifie que les mesures du premier respectent les frontières du second. Si l'auteur change pour Commons.France1995.LaQuadrature, le bridge vérifie les nouvelles frontières — sans aucune modification du code, juste en changeant le <PackageReference>. Le bridge est agnostique aux cadres concrets parce qu'il ne référence que les interfaces racines (ILegalContainer, IAssetStatus, IBoundary...) ; il fonctionne identiquement pour n'importe quelle paire (Law.X, Commons.Y) choisie. C'est exactement ce qui rend le système politiquement intéressant : l'utilisateur choisit explicitement sa paire de cadres, et le bridge type sa cohérence.
Architecture en cinq sous-projets
Le bridge suit un pattern hexagonal strict, découpé en cinq projets :
Law.Commons.Bridge/
|
+-- Core/ [PUR : no Roslyn, no I/O]
| +-- Identifiers.cs (AssetFqn, SpaceFqn, PeriodFqn... records typés)
| +-- CrossModel.cs (records sérialisables FQN-based)
| +-- FrontierCrossingDetector.cs (fonction pure CrossModel -> violations)
| +-- Guards/ (un Guard par invariant cross-DSL)
| +-- Simulation/ (PolicyScenario, ScenarioRunner, ConsensusScorer)
| +-- Verdicts/ (CrossingViolation, ValidatedCrossing)
|
+-- Roslyn/ [extraction Roslyn pure]
| +-- CrossModelExtractor.cs (Compilation -> CrossModel)
| +-- LocationIndex.cs (table des Locations par (ArticleFqn, AlineaIndex, ViolationCode))
| +-- RoslynAnalyzerExtensions.cs (ImplementsInterface, HasAttribute, etc.)
|
+-- SourceGenerator/ [IIncrementalGenerator -> build-time]
| +-- LawCommonsBridgeGenerator.cs
| +-- BridgeDiagnostics.cs (LOI001..LOI099)
| +-- CrossModelEmitter.cs (Scriban -> Law.Commons.Cross.g.cs)
|
+-- Analyzers/ [DiagnosticAnalyzer LIVE + CodeFixers]
| +-- LawCommonsCoherenceAnalyzer.cs (mince : Compilation -> Extractor -> Detector -> ReportDiagnostic)
| +-- CoherenceDiagnostics.cs (CON_001..CON_099)
| +-- CodeFixers/
| +-- MissingProcedureCodeFix.cs
| +-- SuggestKnownProceduresCodeFix.cs
|
+-- Cli/ [CLI optionnel]
+-- Program.cs (System.CommandLine)
+-- Commands/{Validate,Simulate,Consensus,TimeDiff}Command.csLaw.Commons.Bridge/
|
+-- Core/ [PUR : no Roslyn, no I/O]
| +-- Identifiers.cs (AssetFqn, SpaceFqn, PeriodFqn... records typés)
| +-- CrossModel.cs (records sérialisables FQN-based)
| +-- FrontierCrossingDetector.cs (fonction pure CrossModel -> violations)
| +-- Guards/ (un Guard par invariant cross-DSL)
| +-- Simulation/ (PolicyScenario, ScenarioRunner, ConsensusScorer)
| +-- Verdicts/ (CrossingViolation, ValidatedCrossing)
|
+-- Roslyn/ [extraction Roslyn pure]
| +-- CrossModelExtractor.cs (Compilation -> CrossModel)
| +-- LocationIndex.cs (table des Locations par (ArticleFqn, AlineaIndex, ViolationCode))
| +-- RoslynAnalyzerExtensions.cs (ImplementsInterface, HasAttribute, etc.)
|
+-- SourceGenerator/ [IIncrementalGenerator -> build-time]
| +-- LawCommonsBridgeGenerator.cs
| +-- BridgeDiagnostics.cs (LOI001..LOI099)
| +-- CrossModelEmitter.cs (Scriban -> Law.Commons.Cross.g.cs)
|
+-- Analyzers/ [DiagnosticAnalyzer LIVE + CodeFixers]
| +-- LawCommonsCoherenceAnalyzer.cs (mince : Compilation -> Extractor -> Detector -> ReportDiagnostic)
| +-- CoherenceDiagnostics.cs (CON_001..CON_099)
| +-- CodeFixers/
| +-- MissingProcedureCodeFix.cs
| +-- SuggestKnownProceduresCodeFix.cs
|
+-- Cli/ [CLI optionnel]
+-- Program.cs (System.CommandLine)
+-- Commands/{Validate,Simulate,Consensus,TimeDiff}Command.csBridge.Core est le domaine pur. Pas de dépendance Roslyn, pas d'I/O, pas d'accès fichier. Juste des records immuables et des fonctions pures. C'est ici que vit toute la logique de détection de violation. Un test unitaire sur le Core n'a besoin de rien d'autre que de construire un CrossModel en mémoire et d'appeler FrontierCrossingDetector.Analyze().
Bridge.Roslyn est le port d'entrée. Il sait transformer un Compilation Roslyn (l'arbre syntaxique complet du projet analysé) en un CrossModel (le modèle pur que le Core comprend). C'est le seul endroit du bridge qui connaît Roslyn.
Bridge.SourceGenerator et Bridge.Analyzers sont deux adapters qui exposent le même domaine Core à deux contrats Roslyn différents. Le SG tourne au build-time et produit des artefacts durables ; les Analyzers tournent en IDE-time et produisent des diagnostics éphémères. Les deux appellent FrontierCrossingDetector.Analyze() sur le même CrossModel extrait par le même CrossModelExtractor. Aucune duplication de logique.
Bridge.Cli est un outil en ligne de commande optionnel qui expose les mêmes capacités hors IDE : validate, simulate, consensus, time-diff. Il est utile pour les scripts CI/CD et pour les utilisateurs qui préfèrent le terminal à l'IDE.
Les identifiants FQN typés — pourquoi AssetFqn et pas string
Le Core manipule des identifiants pleinement qualifiés (FQN) pour désigner les types des cadres analysés. Mais il ne les manipule jamais comme des string brutes. Chaque famille d'identifiant a son propre type record :
namespace FrenchExDev.Net.Law.Commons.Bridge.Core;
// Interface marqueur pour tous les FQN typés
public interface ITypedFqn { string Value { get; } }
// Un FQN par famille de concept
public sealed record AssetFqn (string Value) : ITypedFqn;
public sealed record SpaceFqn (string Value) : ITypedFqn;
public sealed record PeriodFqn (string Value) : ITypedFqn;
public sealed record ProcedureFqn(string Value) : ITypedFqn;
public sealed record ArticleFqn (string Value) : ITypedFqn;
public sealed record AlineaFqn (string Value) : ITypedFqn;namespace FrenchExDev.Net.Law.Commons.Bridge.Core;
// Interface marqueur pour tous les FQN typés
public interface ITypedFqn { string Value { get; } }
// Un FQN par famille de concept
public sealed record AssetFqn (string Value) : ITypedFqn;
public sealed record SpaceFqn (string Value) : ITypedFqn;
public sealed record PeriodFqn (string Value) : ITypedFqn;
public sealed record ProcedureFqn(string Value) : ITypedFqn;
public sealed record ArticleFqn (string Value) : ITypedFqn;
public sealed record AlineaFqn (string Value) : ITypedFqn;L'avantage est immédiat : le compilateur refuse AssetFqn == ProcedureFqn. Impossible de comparer accidentellement un bien avec une procédure. F12 navigable. Tests lisibles : new AssetFqn("global::Common.France2026.Etalab.Assets.DrinkingWater"). Et dans les jointures du détecteur, la signature force la bonne combinaison — model.Assets.FirstOrDefault(b => b.Asset == measure.TargetAsset) ne compile que si les deux côtés sont des AssetFqn.
La couche Bridge.Roslyn fournit les méthodes d'extraction qui convertissent un INamedTypeSymbol Roslyn en FQN typé :
internal static class FqnExtensions
{
public static AssetFqn ToAssetFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
public static SpaceFqn ToSpaceFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
public static ProcedureFqn ToProcedureFqn(this INamedTypeSymbol s) => new(s.ToFqnString());
public static ArticleFqn ToArticleFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
private static string ToFqnString(this INamedTypeSymbol s) =>
s.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}internal static class FqnExtensions
{
public static AssetFqn ToAssetFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
public static SpaceFqn ToSpaceFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
public static ProcedureFqn ToProcedureFqn(this INamedTypeSymbol s) => new(s.ToFqnString());
public static ArticleFqn ToArticleFqn (this INamedTypeSymbol s) => new(s.ToFqnString());
private static string ToFqnString(this INamedTypeSymbol s) =>
s.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}C'est le même principe que Partie 6 : aucune chaîne magique n'est jamais pivot. Mais ici, la discipline est encore plus stricte parce que le bridge joint des concepts de cadres différents — une erreur de jointure entre un AssetFqn et un ProcedureFqn serait catastrophique et silencieuse avec des strings, impossible et bruyante avec des records typés.
Le CrossModel — modèle sérialisable entre Roslyn et Core
Le CrossModel est la structure de données qui circule entre l'extracteur Roslyn et le détecteur pur. Il capture tout ce que le détecteur a besoin de savoir sur les deux cadres joints, sans aucune dépendance Roslyn :
public sealed record CrossModel(
IReadOnlyList<LawArticleModel> Articles,
IReadOnlyList<CommonsAssetStatusModel> Assets,
IReadOnlyList<CommonsBoundaryModel> Boundaries);
public sealed record LawArticleModel(
ArticleFqn ContainingType,
string Number,
SpaceFqn? Space,
PeriodFqn? Period,
IReadOnlyList<LawAlineaModel> Alineas);
public sealed record LawAlineaModel(
int Index,
AlineaFqn Method,
IReadOnlyList<LawMeasureModel> Measures,
IReadOnlyList<LawProcedureModel> Procedures);
public sealed record LawMeasureModel(
MeasureKindCode Kind,
AssetFqn TargetAsset);
public sealed record LawProcedureModel(
ProcedureFqn Procedure,
string? ReferenceDate);
public sealed record CommonsAssetStatusModel(
AssetFqn Asset,
IReadOnlyList<CommonsStatusModel> Statuses);
public sealed record CommonsStatusModel(
StatusKindCode Kind,
SpaceFqn Space,
PeriodFqn Period);
public sealed record CommonsBoundaryModel(
StatusKindCode From,
StatusKindCode To,
ProcedureFqn RequiredProcedure);public sealed record CrossModel(
IReadOnlyList<LawArticleModel> Articles,
IReadOnlyList<CommonsAssetStatusModel> Assets,
IReadOnlyList<CommonsBoundaryModel> Boundaries);
public sealed record LawArticleModel(
ArticleFqn ContainingType,
string Number,
SpaceFqn? Space,
PeriodFqn? Period,
IReadOnlyList<LawAlineaModel> Alineas);
public sealed record LawAlineaModel(
int Index,
AlineaFqn Method,
IReadOnlyList<LawMeasureModel> Measures,
IReadOnlyList<LawProcedureModel> Procedures);
public sealed record LawMeasureModel(
MeasureKindCode Kind,
AssetFqn TargetAsset);
public sealed record LawProcedureModel(
ProcedureFqn Procedure,
string? ReferenceDate);
public sealed record CommonsAssetStatusModel(
AssetFqn Asset,
IReadOnlyList<CommonsStatusModel> Statuses);
public sealed record CommonsStatusModel(
StatusKindCode Kind,
SpaceFqn Space,
PeriodFqn Period);
public sealed record CommonsBoundaryModel(
StatusKindCode From,
StatusKindCode To,
ProcedureFqn RequiredProcedure);Deux enums miroirs complètent le modèle — MeasureKindCode et StatusKindCode — qui sont des copies des enums de Common.Dsl sans la dépendance Roslyn. Le bridge SG fait la conversion lors de l'extraction.
Ce CrossModel est sérialisable (tous les types sont des records avec des primitifs ou d'autres records). Il peut être sauvegardé en JSON pour audit, transmis à un ScenarioRunner pour simulation what-if, comparé entre deux versions pour time-diff. La sérialisabilité n'est pas un accident — c'est ce qui permet au système de produire des preuves vérifiables que tel cas a été analysé contre tel cadre à tel moment.
FrontierCrossingDetector — la fonction pure au cœur du système
Le détecteur est une fonction statique pure. Il prend un CrossModel, retourne une liste de CrossingViolation. Pas d'effets de bord, pas de state, pas d'I/O. Testable en une ligne.
L'algorithme est simple dans sa structure, profond dans ses implications. Pour chaque article, pour chaque alinéa de cet article, pour chaque mesure de cet alinéa :
Jointure par bien : la mesure cible un
AssetFqn. Le détecteur cherche unCommonsAssetStatusModelqui porte le mêmeAssetFqn. Si aucun statut n'est trouvé, violationLOI001(warning) — le bien est typé dans le cadre Law mais n'a pas de statut dans le cadre Commons choisi.Filtrage par espace : si l'article porte un
Space, seuls les statuts applicables à cet espace sont retenus. Si aucun statut applicable n'est trouvé, violationLOI004.Statut courant : le statut le plus récent applicable est sélectionné.
Calcul du statut cible : à partir du
MeasureKindet du statut courant, le détecteur calcule le statut cible. UnePrivatisationciblePrivate, uneNationalisationciblePublic, uneRestorationcibleCommons, uneSuppressioncibleContested, etc. Si le statut courant et le statut cible sont identiques, la mesure est un no-op et on passe à la suivante.Recherche de frontière : le détecteur cherche une
CommonsBoundaryModelqui couvre la transitionFrom → To. Si aucune frontière n'est déclarée, violationLOI002(error) — le cadre Commons ne reconnaît pas cette transition.Vérification de procédure : si une frontière est trouvée, elle exige une procédure démocratique (
RequiredProcedure). Le détecteur vérifie qu'une[DemocraticProcedure]avec le bonProcedureFqnest déclarée sur l'alinéa. Si elle est absente, violationLOI003(error) — c'est le compile error politique fondamental du système, celui qui se déclenche sur le cas Dumas (cf. Partie 10).
public sealed record CrossingViolation(
ViolationCode Code,
ViolationSeverity Severity,
ArticleFqn Article,
int Alinea,
string Message);
public enum ViolationCode
{
LOI001_AssetWithoutStatus,
LOI002_UndeclaredBoundary,
LOI003_MissingProcedure,
LOI004_StatusOutOfScope,
LOI005_WrongProcedureType,
LOI006_ProcedureTooShort,
LOI007_UndocumentedRestoration,
LOI008_PeriodOutOfStatuses
}public sealed record CrossingViolation(
ViolationCode Code,
ViolationSeverity Severity,
ArticleFqn Article,
int Alinea,
string Message);
public enum ViolationCode
{
LOI001_AssetWithoutStatus,
LOI002_UndeclaredBoundary,
LOI003_MissingProcedure,
LOI004_StatusOutOfScope,
LOI005_WrongProcedureType,
LOI006_ProcedureTooShort,
LOI007_UndocumentedRestoration,
LOI008_PeriodOutOfStatuses
}Huit codes de violation, trois niveaux de sévérité (Info, Warning, Error). Le LOI003_MissingProcedure est le plus important politiquement : il dit que cette mesure change le statut d'un bien sans la procédure démocratique que le cadre politique choisi exige. Le cas Dumas 1995 produit exactement ce LOI003.
Trois familles d'analyzers — trois moments d'écoute
L'architecture comporte trois familles d'analyzers distinctes, chacune produite par un mécanisme différent et opérant dans un contexte différent.
1. Les analyzers structurels par cadre
Pour chaque cadre Law.${Space}${Time}.${Author} que LawDslGenerator produit, un ${Author}${Space}${Time}StructuralAnalyzer est automatiquement généré à partir des LegalSpecification déclarées dans la LegalDslDefinition du cadre. Ces analyzers vérifient la cohérence interne du cadre : un [Alinea] ne doit pas être posé hors d'un [Article], un [Measure] doit cibler un IAsset, un [Article] doit avoir son Space et sa Period.
Chaque cadre a les siens, avec ses propres diagnostics préfixés (LAW_FR2026_ETALAB_TYP_* pour Etalab France 2026, LAW_FR2026_LAQUADRATURE_TYP_* pour LaQuadrature France 2026). Deux cadres concurrents pour le même (Space, Time) peuvent avoir des spécifications structurelles différentes, et leurs diagnostics restent distincts et identifiables. L'analyzer est généré — son code n'a jamais à être écrit à la main par l'auteur du cadre.
C'est le premier niveau d'écoute : l'auteur d'un cas reçoit un retour immédiat sur la conformité de son code aux spécifications du cadre dans lequel il travaille.
2. Les analyzers de cohérence inter-cadres
Ce sont les analyzers qui vivent dans les bridges. LawCommonsCoherenceAnalyzer vérifie en temps réel la cohérence entre un cadre Law.* et le cadre Commons.* qu'il référence. LawCitizenCoherenceAnalyzer fait la même chose entre Law et Citizen : il prend un cas citoyen et vérifie quels articles lui sont applicables, émettant des CIT* qui guident l'utilisateur vers ses droits.
Ces analyzers sont systématiquement minces (~80 LOC chacun). Ils ne contiennent aucune logique métier — ce sont des adapters Roslyn qui appellent les méthodes pures de Bridge.Core. La logique est dans le Core, partagée avec le SG correspondant. Pattern Hexagonal strict.
Le pipeline d'un analyzer inter-cadres est linéaire : Compilation (Roslyn) -> CrossModelExtractor (Bridge.Roslyn) -> FrontierCrossingDetector (Bridge.Core) -> ReportDiagnostic (Roslyn). Trois appels, zéro logique métier dans l'analyzer lui-même.
C'est le deuxième niveau d'écoute : la cohérence entre cadres concurrents ou complémentaires, vérifiée en temps réel dans l'IDE.
3. Les analyzers symboliques racines
Ceux-ci vivent à la racine Common.Dsl.Analyzers et vérifient que les symboles déclarés dans n'importe quel cadre Common.${Space}${Time}.${Author} respectent les invariants génériques : sealed partial class, [assembly: SymbolSource("...")] présent, hiérarchie Parent cohérente, période From <= To. Diagnostics SYM_001..SYM_006. Ils ne savent rien des cadres juridiques ; ils contrôlent juste la forme des symboles techniques.
C'est le troisième niveau d'écoute : les invariants structurels de la racine technique, applicables à tous les cadres sans distinction.
La dualité SG / Analyzer — l'analyzer enseigne, le SG atteste
Cette dualité n'est pas une duplication — c'est une séparation des moments d'écoute fondamentalement différents.
Le Source Generator s'exécute au moment du dotnet build. Il a accès à tout le projet en une seule passe, ce qui signifie qu'il peut produire un audit trail global (Law.Commons.Cross.g.cs, LawQualityReport.json, un dashboard HTML) qu'aucun analyzer n'aurait la cohérence pour produire. Il s'exécute moins souvent (à chaque build, pas à chaque touche), donc il peut se permettre des analyses coûteuses comme la simulation ScenarioRunner ou le ConsensusScorer inter-cadres. Et sa sortie est durable — versionnée par git, signée cryptographiquement, attachée à l'attestation de build. C'est ce qui sert de preuve juridique qu'un cas a été vérifié dans tel cadre à tel moment.
Les analyzers, eux, s'exécutent à chaque frappe dans l'IDE. Ils donnent un retour en moins de 100 millisecondes, ce qui est l'horizon de la pensée fluide — l'utilisateur reste dans le flow. Ils ne touchent que la portion de code modifiée (incrémental) — impossible de leur faire faire des simulations coûteuses. Et leur sortie est éphémère — elle vit dans la liste des "Problèmes" du panneau IDE, n'est pas versionnée, n'est pas signée, n'a aucune valeur juridique. Mais c'est elle qui éduque l'utilisateur en temps réel.
Un cas concret illustre cette dualité. Mathilde tape son fichier MathildeCase.cs dans Lex Studio (cf. Partie 15). À chaque ligne ajoutée, les analyzers tournent et soulignent en rouge ce qui est mal-formé dans le cadre Law.France2026.Etalab qu'elle utilise (analyzer structurel), et émettent des CIT* informationnels qui lui signalent quels droits elle active (analyzer de cohérence inter-cadres). Quand elle sauvegarde et lance dotnet build, le SG tourne, produit l'attestation cryptographique signée, et matérialise dans obj/Debug/.../generated/MathildeCase.Cross.g.cs la liste exhaustive des articles applicables avec leurs sources Légifrance.
L'analyzer enseigne ; le SG atteste. Les deux moments sont indispensables et non-substituables.
Les CodeFixers — l'autre moitié de l'analyzer
Un DiagnosticAnalyzer Roslyn ne peut que signaler — il ne peut pas modifier le code. Pour modifier le code, il faut un CodeFixProvider enregistré avec l'analyzer. Pour chaque famille de diagnostic, le système livre un ou plusieurs CodeFixers.
| Fixer | Diagnostic cible | Ce qu'il fait |
|---|---|---|
MissingProcedureCodeFix |
CON_003 / LOI003 | Ajoute [DemocraticProcedure(typeof(...))] au-dessus du [Measure] |
SuggestKnownProceduresCodeFix |
CON_005 / LOI005 | Liste les IProcedure connus du cadre Common.* référencé |
MakeSealedPartialFix |
SYM_001, SYM_002 | Ajoute sealed partial à la classe |
AddSymbolSourceMarkerFix |
SYM_005 | Insère [assembly: SymbolSource("...")] |
AddArticleSpaceFix |
LAW_FR2026_TYP_005 | Suggère les ISpace connus |
WrapMethodInArticleFix |
LAW_FR2026_TYP_006 | Encapsule la méthode dans une classe [Article] |
ActivateRightCodeFix |
CIT* (régime 6) | Ajoute [Activate(typeof(...))] et génère MySteps.g.md |
C'est ce qui transforme l'analyzer d'un feu rouge passif en assistant actif. Le diagnostic souligne en rouge, le Quick Fix (Ctrl+. dans VS Code) propose une correction adaptée. Pour Mathilde, le CodeFixer ActivateRightCodeFix ne se contente pas de dire « vous avez un droit non-activé » — il propose le geste pour l'activer et génère les étapes administratives. Le compilateur devient un coach, pas un auditeur.
Le pattern Guard — un nom, un check
Tous les analyzers du système suivent le même pattern : un nom = un check, signature uniforme XxxGuard(ctx, subject, [context]), retour void, early-return sur les cas non-applicables. L'orchestrateur Analyze* ne fait que dispatcher.
// Un guard vérifie un seul invariant, avec early-return
private static void MeasureAssetTypeGuard(
SymbolAnalysisContext ctx, AttributeData attr, INamedTypeSymbol iAsset)
{
if (attr.ConstructorArguments.Length < 2) return;
if (attr.ConstructorArguments[1].Value is not INamedTypeSymbol assetType) return;
if (assetType.ImplementsInterface(iAsset)) return;
ctx.ReportDiagnostic(Diagnostic.Create(
LawFrance2026Diagnostics.TYP_001,
attr.GetLocationOrNone(),
assetType.Name));
}
// L'orchestrateur fait juste du dispatch
private static void AnalyzeMethod(SymbolAnalysisContext ctx, /* ... */)
{
var method = (IMethodSymbol)ctx.Symbol;
foreach (var attr in method.GetAttributes())
{
switch (attr.AttributeClass?.ToDisplayString())
{
case MeasureAttrFqn: MeasureAssetTypeGuard(ctx, attr, iAsset); break;
case DemProcedureAttrFqn: DemocraticProcedureTypeGuard(ctx, attr, iProcedure); break;
}
}
AlineaOutsideArticleGuard(ctx, method);
MeasureWithoutAlineaGuard(ctx, method);
}// Un guard vérifie un seul invariant, avec early-return
private static void MeasureAssetTypeGuard(
SymbolAnalysisContext ctx, AttributeData attr, INamedTypeSymbol iAsset)
{
if (attr.ConstructorArguments.Length < 2) return;
if (attr.ConstructorArguments[1].Value is not INamedTypeSymbol assetType) return;
if (assetType.ImplementsInterface(iAsset)) return;
ctx.ReportDiagnostic(Diagnostic.Create(
LawFrance2026Diagnostics.TYP_001,
attr.GetLocationOrNone(),
assetType.Name));
}
// L'orchestrateur fait juste du dispatch
private static void AnalyzeMethod(SymbolAnalysisContext ctx, /* ... */)
{
var method = (IMethodSymbol)ctx.Symbol;
foreach (var attr in method.GetAttributes())
{
switch (attr.AttributeClass?.ToDisplayString())
{
case MeasureAttrFqn: MeasureAssetTypeGuard(ctx, attr, iAsset); break;
case DemProcedureAttrFqn: DemocraticProcedureTypeGuard(ctx, attr, iProcedure); break;
}
}
AlineaOutsideArticleGuard(ctx, method);
MeasureWithoutAlineaGuard(ctx, method);
}Lisible en une ligne, testable individuellement, composable, cherchable (grep -r "Guard" liste tous les invariants vérifiés du système — c'est la spec exécutable de la DSL). Le pattern est documenté dans la série FrenchExDev Patterns et appliqué identiquement ici.
Pourquoi cette architecture compte démocratiquement
Les analyzers sont l'endroit où la métacratie devient personnelle. Sans eux, le projet serait un système de batch processing : on écrit un cas, on lance un build, on lit un rapport. Avec eux, le système devient un partenaire de pensée : on écrit son cas et le système réagit en direct, montre les implications, suggère des pistes, signale les zones de vide juridique. C'est la différence entre un auditeur et un coach. Et c'est cette différence qui rend l'outillage politiquement intéressant pour les destinataires (citoyens, juristes), pas seulement pour les producteurs (législateurs, chercheurs).
Note opérationnelle : chaque cadre généré par LawDslGenerator apporte ses analyzers avec lui. Quand un nouvel auteur publie un cadre concurrent Law.France2026.MyOrg.Dsl, ses analyzers sont automatiquement disponibles pour quiconque référence ce cadre dans son csproj. La barrière d'entrée pour offrir une expérience IDE complète est zéro — les analyzers sont générés, pas écrits à la main. C'est ce qui permet la prolifération démocratique des cadres juridiques : créer un cadre concurrent ne coûte pas seulement le code des spécifications, ça apporte aussi gratuitement le retour interactif dans l'IDE pour ses utilisateurs.
Le bridge vérifie la cohérence interne de chaque paire choisie. Le ConsensusScorer, qui vit dans Bridge.Core/Simulation/, exhibe les divergences entre cadres concurrents. Et le choix du cadre reste un acte politique explicite, matérialisé par un <PackageReference> dans le csproj — un acte versionné par git, traçable, réversible, diffable. Ce n'est pas un paramètre enfoui dans une configuration cachée. C'est la première ligne du fichier projet, visible par quiconque ouvre le repository.
La Partie 10 montrera cette architecture en action, de bout en bout, sur le cas Dumas 1995 — du csproj au compile error rouge dans Visual Studio Code.
Pour aller plus loin
- Partie 8 —
Commons.DsletCitizen.Dsl(precedente) - Partie 10 — Trace d'exécution : le cas Dumas 1995 (suivante)
- Hub de la serie
- Partie 5 — Architecture en couches pour le pattern hexagonal general
- FrenchExDev Patterns — Guard pour le pattern Guard réutilisé ici
- Serie Ops DSL Ecosystem pour le même pattern Attribute + SG + Analyzer appliqué à l'opérationnel