Pourquoi commencer par le vocabulaire
Cette série va répéter une dizaine de termes, page après page. Si l'on ne les pose pas une fois, proprement, ils deviendront des éponges sémantiques. Métamodèle veut tantôt dire « grammaire d'un DSL », tantôt « système de types fort », tantôt « modèle au-dessus du modèle ». Transformation couvre du XSLT, du Babel-plugin, des matched-rules ATL, des templates Acceleo, des IIncrementalGenerator Roslyn. Si l'on n'arrête pas un usage, on parlera dix langues différentes — et la promesse de traçabilité bout-en-bout que défend cette série tombera dès la troisième page.
On fixe donc le vocabulaire ici. Les choix sont prescriptifs pour cette série uniquement. Là où ils empruntent à un outil existant, c'est dit explicitement (ATL, QVT, Epsilon, Roslyn) et le détail va dans Part 02.
La pile OMG, version utilisable
L'Object Management Group a fixé une pile à quatre étages dans MOF 2.0 : M0 (objets concrets), M1 (modèles), M2 (métamodèles), M3 (metameta-model, MOF lui-même). C'est ce qui circule dans tous les papiers MDE depuis vingt ans. La pile est utile mais souvent enseignée en l'air — on ne voit pas ce qui change concrètement entre les étages. Dans cette série, on la rebranche sur du TypeScript exécutable.
Lecture critique du diagramme. Trois choses méritent l'œil avant le contenu. Le M3 est auto-typé (la flèche pointillée qui boucle) : la définition de ce qu'est un metamodel est elle-même conforme à ce qu'on appelle un metamodel — c'est un fixpoint conceptuel hérité de MOF. Les arêtes portent des verbes différents (définit, instancie, transforme) parce que ce ne sont pas la même opération : instancier un M2 (écrire @AggregateRoot class Quote {}) coûte une ligne ; transformer un M1 en M0 coûte un programme entier. Et le sens des flèches va vers le bas : un niveau N définit la grammaire du niveau N-1 ; mais le travail concret va dans l'autre sens, du M1 qu'on écrit vers le M0 qu'on émet.
M0 — les fichiers que la CI commite
C'est la sortie. Ici, dans cette série, ce sont les fichiers TypeScript du dossier packages/openstore-* après que le pipeline a tourné. Banner @generated, commentaire // @trace ruleId=… source=… target=… au-dessus de chaque déclaration, ImplementationSlot aux endroits que la générativité ne peut pas remplir. Lisible par un humain, modifiable par une IA, garanti idempotent par le banner SHA256 de ts-codegen-pipeline.
M1 — le modèle métier que vous écrivez à la main
C'est l'entrée. Pour OpenStore, c'est assets/m1-openstore.ts : une description déclarative en TypeScript de ce que fait le shop (capabilities B2B et B2C, tenancy, channels). Aucune logique, aucun runtime — juste des classes décorées, dont le seul rôle est d'être lues par un transformer. C'est le moment du Domain-Driven Design au sens littéral : on décrit le domaine, pas la solution.
M2 — les métamodèles qui définissent la grammaire des M1
Ce sont les vocabulaires intermédiaires. Un M2 est un triplet (Concepts, Relations, Constraints). DDD est un M2 : ses Concepts sont AggregateRoot, Entity, ValueObject, etc. ; ses Relations sont composes, emits, enforces ; ses Constraints sont des invariants vérifiables sur un M1 conforme. CQRS, VerticalSlice, Telemetry sont trois autres M2 distincts. La série en utilise quatre simultanément, qu'elle apprend à composer dans Part 08.
M3 — le cadre qui définit ce qu'est un métamodèle
C'est l'apport éditorial principal de la série. Le M3 n'est pas un nouveau metamodel ; c'est le vocabulaire commun pour décrire des metamodèles. Le fichier assets/cadre.ts le pose en TypeScript : Concept, DecoratorSpec, Relation, Constraint, M2Schema, CrossRelation, ConflictPolicy, Composition, Rule, Helper, Guard, Trace, Transformation, Emitter, Megamodel, et TransformerGenerator. Quinze constructs. Pas un de plus, pas un de moins. C'est la liste qui fait tenir la suite.
Le vocabulaire emprunté
Le M3 n'invente rien. Il agrège des concepts éprouvés.
Rule — l'atome de transformation (emprunt ATL)
Une rule projette une instance d'un Concept source vers une instance d'un Concept cible. Trois variantes, héritées d'ATL : matched (s'applique automatiquement à toute instance source qui passe son guard), lazy (déclenchée à la demande, pour les Concepts pointés par des références), called (appelée explicitement comme une méthode, paramètres possibles). La distinction est rendue typée dans cadre.ts par un champ kind: 'matched' | 'lazy' | 'called'. Toute Rule porte un id, est typée Rule<TFrom, TTo>, et émet une Trace dans son TransformContext à chaque application.
Helper — la factorisation pure (emprunt ATL)
Un helper est une fonction pure que les rules partagent. Il est cacheable parce que side-effect free. Dans cette série, assets/transformer-composite.ts en déclare deux : pascalCase (utilitaire de nommage) et aggregateNameFromUseCase (qui mappe une use-case d'OpenStore vers le nom de l'aggregate cible). C'est ce qu'on appelle ailleurs un utility function. ATL en a fait une primitive de premier ordre parce que sans helpers, les matched-rules deviennent illisibles. On le copie ici sans bouger.
Guard — la prédicate sur la rule (emprunt Epsilon)
Un guard est un prédicat optionnel sur une rule : la rule n'applique son corps que si le guard renvoie true. C'est ce qui permet de spécialiser une matched-rule sans la dédoubler. Epsilon en a fait un construct first-class avec une syntaxe minimaliste (: <expression> ou un bloc complet). Dans cadre.ts, c'est un objet typé Guard<TFrom> avec une seule méthode evaluate(source: TFrom): boolean.
Trace — la traçabilité QVT, sans bidirectionnalité
Une trace est un triplet (ruleId, sourceElementId, targetElementId) — qui a transformé quoi en quoi. QVT-Relations en a fait l'épine dorsale de sa proposition bidirectionnelle. Le cadre proposé garde la trace, abandonne la bidirectionnalité (raison : aucun cas d'usage observé de round-trip M1 ↔ code dans le monde TypeScript). Les traces sont émises pendant la transformation par TransformContext.emit(trace), persistées en mémoire pendant le pipeline, et inscrites en commentaire au-dessus de chaque déclaration générée — c'est la promesse de la série.
Aspect — la contribution partielle à un M1
Un aspect est une contribution partielle à un M1 final. La série en a besoin parce que Part 08 montre que M1-WebApp est l'agrégation de trois aspects : Slices ⊕ FastifyConfig ⊕ AuthAspect. L'idée vient de l'aspect-oriented programming (AspectJ) ; on la récupère sous une forme dégradée : un Aspect<TBase, TLayer> qui contribue à TBase un TLayer typé.
Merge — l'opération qui combine plusieurs aspects
Un merge prend N inputs et produit un output. Sa subtilité tient à la politique de résolution des conflits. Quand un Concept porte le même nom dans deux M2Schemas (le cas réel : @DomainEvent est à la fois dans DDD et dans CQRS), la ConflictPolicy détermine la fusion : share (un seul Concept partagé, contributions unionnées), prefer:Schema (un schema l'emporte), rename (on renomme). Part 08 en montre l'application sur OpenStore.
Megamodel — la description du pipeline complet (emprunt ProMoTA)
Un megamodel est un modèle dont les éléments sont eux-mêmes des modèles, des metamodèles et des transformations. C'est l'objet qui répond à la question : « quelle est la chaîne complète de mon projet ? » L'approche est défendue par ProMoTA pour la traçabilité bout-en-bout en MDE. Dans cadre.ts, le Megamodel agrège schemas, compositions, transformations, emitters, requirements, features, acceptanceCriteria, qualityGates, et un chain: ReadonlyArray<string> qui définit l'ordre d'orchestration. C'est l'objet que Part 10 traite comme citoyen de première classe.
Ce qui distingue cette série du « MDE classique »
Trois choix de la série méritent d'être pointés tôt, parce qu'ils contredisent des présupposés ancrés.
Pas de bidirectionnel. Les transformations vont dans un sens : M1 source → M1 cible, M1 → code. Pas de round-trip. La raison est observationnelle : aucune équipe TypeScript croisée n'utilise QVT-Relations bidirectionnel en production, et les rares qui ont essayé en sont revenues. Le coût de la cohérence bidirectionnelle excède son bénéfice dès que les transformations ne sont pas surjectives (et elles ne le sont presque jamais).
Pas de génération hermétique. La pipeline laisse délibérément des trous. Le M2M génératif produit un squelette de lib : interfaces, types, signatures, contrats. Les bodies des Handlers, Domain Services, Projections sont des ImplementationSlot explicites. Une IA ou un humain les remplit, sous garde de quality gates dérivées des AcceptanceCriterion. Cette honnêteté est le contraire de la promesse « low-code », qui prétend franchir une frontière qu'aucune description structurelle ne peut franchir.
Pas de centralisation du M3. Le M3 ici décrit n'est pas un standard à imposer. C'est une manière de poser le vocabulaire, suffisante pour faire tenir le mini-shop OpenStore sur dix chapitres. D'autres M3 sont possibles ; certains seront meilleurs. Le geste utile est de poser un cadre, le faire fonctionner sur un cas réel, et le publier en lecture critique avant qu'il existe comme package.
Récapitulatif des termes que la suite utilise
| Terme | Origine | Définition de travail |
|---|---|---|
| Concept | OMG MOF | Nom typé d'une entité d'un M2 (AggregateRoot, Command, Slice, Span). |
| DecoratorSpec | TypeScript / TC39 | Annotation TS qui matérialise un Concept à la frontière M1. |
| Relation | OMG MOF | Lien orienté typé entre deux Concepts au sein d'un M2Schema. |
| CrossRelation | propre à la série | Lien orienté typé entre Concepts de M2Schemas distincts (ex. CQRS.CommandHandler ─touches→ DDD.AggregateRoot). |
| Constraint | OCL / Epsilon | Prédicat sur un M1 conforme à un M2Schema. |
| M2Schema | propre à la série | Triplet (Concepts, Relations, Constraints). Un metamodel complet. |
| ConflictPolicy | propre à la série | Règle de résolution quand deux M2 définissent un Concept de même nom. |
| Composition | propre à la série | Tuple (M2Schemas[], CrossRelations[], ConflictPolicy[]). Le produit relationnel. |
| Rule | ATL | Fonction partielle M1[ConceptSource] → M1[ConceptTarget], avec kind et guard. |
| Helper | ATL | Fonction pure réutilisable et cacheable. |
| Guard | Epsilon, ATL | Prédicat optionnel sur une Rule. |
| Trace | QVT, ProMoTA | Triplet (ruleId, sourceElementId, targetElementId). |
| Transformation | OMG, ATL | Ensemble de Rules + Helpers. Entrée : M1 source. Sortie : M1 cible + Traces. |
| Aspect | AspectJ | Contribution partielle à un M1 final. |
| Merge | propre à la série | Opération N-aire (M1, …) → M1 avec ConflictPolicy. |
| Emitter | Roslyn, ts-codegen-pipeline | Transformation dont la cible est « texte TypeScript ». |
| Megamodel | ProMoTA | Description du pipeline complet (schemas + transformations + emitters + requirements + chain). |
| TransformerGenerator | propre à la série | Opérateur (Composition × M2Schema_target) → code TS d'une Transformation. Le M3 lui-même. |
| RequirementSpec | propre à la série, aligné requirements-lib |
Besoin fonctionnel formel attaché à un Concept M1. |
| FeatureSpec | id. | Feature dérivée par le transformer ; lie un Requirement à un set de Slices/Handlers. |
| AcceptanceCriterion | id. | Prédicat exécutable sur l'implémentation. |
| ImplementationSlot | propre à la série | Marqueur explicite d'un body à fournir, avec contrat (pré/post/AC). |
| QualityGate | id. | Fonction qui vérifie qu'une implémentation passe ses AC. |
Pour aller plus loin
- Part 02 — État de l'art : ATL, QVT, Acceleo, Epsilon, MPS, Roslyn, Langium approfondit chacune des sources empruntées.
- machine-a-projection-v2 — Part 03 : la pile M0-M3 traite la même pile sous l'angle cognitif et philosophique.
- ts-source-generator — Part 12 : Ide.Dsl-TS ouvre la perspective d'un kernel TypeScript généralisé construit sur le M3 décrit ici.