Implémentation du noyau

Stratide Flux — Implémentation

Matérialisation Rust du noyau v0.4.1

Avant-propos

Cet article est un compagnon de l’article fondateur de Stratide Flux. L’article fondateur pose le régime architectural : ses atomes, ses relations primitives, ses objets dérivés, son régime stratifié, son cadre d’implémentation. Il décrit ce qu’est une architecture lisible pour systèmes d’information.

Le présent texte fait le pas suivant. Il documente la matérialisation Rust du noyau Stratide Flux telle qu’elle existe à la version 0.4.1, c’est-à-dire après six paliers de livraison successifs (v0.1, v0.2, v0.3, v0.4, v0.4.1) et avant l’introduction de la gouvernance globale (v0.5).

Le statut de ce texte est double. Il sert de référence pour celui qui veut comprendre ou auditer le noyau tel qu’il est. Il sert aussi de modèle méthodologique pour celui qui voudrait écrire un noyau équivalent dans un autre langage : ce qui doit être typé, ce qui doit être séparé, ce qui doit rester optionnel, ce qui doit rester ouvert.

Chaque choix d’implémentation présenté ici est une décision contextuelle, prise dans le cadre d’un environnement Rust et d’une posture de noyau. Une autre implémentation, dans un autre contexte, ferait d’autres choix. L’invariant à maintenir est le régime architectural. La présente matérialisation est une lecture parmi d’autres.

L’article comporte six sections. La première situe la posture d’implémentation. La deuxième présente la stratification physique du workspace Rust. Les trois suivantes parcourent les sept modules selon trois familles : les atomes et relations primitives, l’infrastructure des inscriptions, les manifestations contextuelles. La sixième section ouvre sur ce qui reste à livrer.


Section 1 — Posture d’implémentation

Stratide Flux est un noyau. Cette qualification est structurante : elle décide tout le reste. Un noyau est ce qui peut être incarné dans plusieurs produits en restant lui-même.

De cette qualification découlent quatre décisions structurelles qui traversent la totalité du code livré.

Première décision : indépendance d’exécution totale. Le workspace Rust se contente de dépendances minimales (uuid, serde, parking_lot, rusqlite, thiserror). Toutes les opérations sont synchrones, déterministes, observables. La conséquence pratique est que cargo test --workspace s’exécute en quelques secondes, sans aucune mise en place d’infrastructure. La conséquence théorique est plus profonde : le noyau reste embeddable dans tout contexte d’exécution, qu’il s’agisse d’un binaire CLI, d’un service backend, d’un agent embarqué WebAssembly ou d’une fonction sans serveur.

Deuxième décision : typage strict des identités. Chaque type d’identifiant a son propre type Rust, distinct par construction des autres. Une NotionId est de type différent d’une ContextId, et une EndpointId de type différent d’une ContractId. La distinction entre identifiants de domaines différents est garantie à la compilation. Cette rigueur fait partie du régime énonciatif lui-même : la disjonction entre les domaines 𝓝 et 𝓚 que pose la théorie est matérialisée dans le système de types du langage.

Troisième décision : sérialisation systématique via serde. Tout type métier est sérialisable en JSON par défaut, et désérialisable. Cette propriété est une exigence du régime : un objet inscriptible, transportable et archivable a sa place dans le noyau. La sérialisation est le seuil de manifestation d’une notion dans un substrat externe, et tout type livré franchit ce seuil.

Quatrième décision : qualité workspace garantie. Quatre commandes valident la base de code à chaque livraison : cargo metadata, cargo fmt --check, cargo clippy -D warnings, cargo test --workspace. Une livraison est validée quand les quatre passent. Cela vaut pour les avertissements clippy : un avertissement traité libère la livraison au même titre qu’un test qui passe. Cette discipline est ce qui permet au noyau de rester lisible au sens fort du mot : un développeur tiers peut auditer, modifier, étendre le code dans un environnement clair.

Ces quatre décisions sont fixes. Elles définissent la posture de noyau. Tout amendement ultérieur du code respecte ces décisions, ou alors change le statut du projet et l’assume publiquement.

Quatre décisions structurelles de la posture noyau D1 — Indépendance d'exécution Dépendances minimales : uuid · serde · parking_lot rusqlite · thiserror Synchrone, déterministe, observable → embeddable CLI / service / WASM D2 — Typage strict des identités Newtype distincts : NotionId ≠ ContextId EndpointId ≠ ContractId ≠ LeaseId Disjonction garantie à la compilation → G1 : 𝓝 ∩ 𝓚 = ∅ D3 — Sérialisation systématique Tout type métier : #[derive(Serialize, Deserialize)] Inscriptible · transportable · archivable Le seuil de manifestation d'une notion dans un substrat externe D4 — Qualité workspace Quatre commandes valident : cargo metadata cargo fmt --check cargo clippy -D warnings cargo test --workspace Une livraison passe quand les 4 passent Ces quatre décisions sont fixes. Tout amendement les respecte ou change publiquement le statut du projet.
Figure 1 — Les quatre décisions structurelles de la posture noyau. Indépendance d'exécution, typage strict, sérialisation systématique, qualité workspace garantie. Ces décisions sont fixes et traversent l'ensemble du code.

À ces décisions structurelles s’ajoute un principe de progression. Le noyau a été livré par paliers, chacun centré sur un module ou une famille d’évolutions, chacun validé par sa propre cartographie produite après compilation. Cette progression a été imposée comme méthode de travail : construire le noyau par accrétion, où chaque palier ferme un sous-régime avant que le suivant en ouvre un autre. La trace de cette méthode est visible dans le découpage des versions, et elle est fidèle au régime énonciatif lui-même : un système se manifeste progressivement.


Section 2 — Stratification physique du workspace

Le noyau est organisé en sept crates Rust, fédérés par un workspace racine. Cette stratification est cohérente : chaque crate correspond à un module théorique de l’architecture, et les dépendances entre crates respectent strictement la hiérarchie des dérivations conceptuelles.

stratide-flux/                          (crate fédérateur)
    │
    ├── stratide-notion-context/        (atomes premiers)
    │
    ├── stratide-substrate-provider/    (infrastructure des inscriptions)
    │
    ├── stratide-inscription/           (relation ▶)
    │
    ├── stratide-movement/              (relation ◁ primitive)
    │
    ├── stratide-endpoint-field/        (centres stabilisés et champs)
    │
    └── stratide-lease-flow/            (manifestations contextuelles)
Workspace Stratide Flux — 7 crates en stratification stratide-flux crate fédérateur — prelude unifié stratide-lease-flow Contract · Lease<P> · Flow · Trace · EnrichedTrace · DormantLifecycle stratide-endpoint-field Endpoint · Field · ReadabilityAssessment · Direction · Registry stratide-inscription relation ▶ — domaine Ω^s stratide-movement relation ◁ — graphe orienté stratide-substrate-provider 4 traits Substrate · 4 implémentations · Provider Engine×Function×Substrate stratide-notion-context Notion · Context · Occurrence — atomes premiers (𝓝, 𝓚, Ω) manifestation centres relations infrastructure atomes Les flèches indiquent les dépendances. Le graphe est acyclique par construction.
Figure 2 — Le workspace Stratide Flux comporte sept crates organisés en hiérarchie acyclique. Chaque crate correspond à un module théorique de l'architecture, et les dépendances respectent strictement les dérivations conceptuelles.

La règle de dépendance est unidirectionnelle. Un crate dépend de crates plus fondamentaux que lui dans la hiérarchie. stratide-lease-flow dépend de stratide-endpoint-field (parce qu’un Contract relie deux Endpoints), qui dépend de stratide-inscription, stratide-movement, stratide-substrate-provider et stratide-notion-context. Le graphe est acyclique par construction : la compilation Rust elle-même garantit l’absence de cycle. Cette discipline protège le régime stratifié de l’architecture jusque dans la disposition physique des fichiers.

Le crate fédérateur stratide-flux est presque exclusivement composé de réexports. Il rassemble les types des six crates spécialisés via un module prelude qui constitue l’interface publique unifiée du noyau. Un utilisateur du noyau écrit use stratide_flux::prelude::*; et obtient l’ensemble des types, traits et fonctions nécessaires pour modéliser un système d’information conforme à Stratide Flux. Cette unification du prelude permet à l’utilisateur de raisonner sur le noyau comme un tout, alors que les implémenteurs raisonnent crate par crate.

L’arbre des dépendances forme un graphe orienté acyclique simple. Le crate stratide-notion-context est la racine : tous les autres crates en dépendent, lui se contente des bibliothèques externes minimales (uuid, serde). À l’autre extrémité, stratide-flux est le puits : tous les autres crates s’y déversent, et il est consommé par les utilisateurs externes du noyau.

Au-delà des sept crates, le workspace héberge également une documentation organisée. Le fichier Cargo.toml racine déclare les versions communes et les dépendances partagées. Le fichier rustfmt.toml impose une largeur maximale de cent caractères et un style de formatage homogène. Les documents ARCHITECTURE.md, MIGRATION.md et les notes de paliers vivent dans docs/. Les cartographies produites après chaque livraison sont archivées séparément, ce qui permet de reconstituer l’historique conceptuel du noyau directement, indépendamment de l’historique git.

Cette organisation physique a une vertu pédagogique : elle est immédiatement lisible. Un développeur qui ouvre le workspace pour la première fois voit la structure, ouvre crates/notion-context/src/lib.rs, comprend qu’il regarde la racine conceptuelle, puis remonte vers les crates plus complexes en suivant les dépendances. Le code est lisible non seulement par sa qualité interne mais par la manière dont il est rangé.


Section 3 — Atomes et relations primitives

La première famille de modules matérialise les fondations conceptuelles : les atomes premiers, l’inscription, le mouvement primitif. Trois crates s’y consacrent, dans cet ordre de dérivation.

3.1 Notion-Context : les atomes premiers

Le crate stratide-notion-context est le plus court et le plus fondamental. Il définit cinq types : Notion, Context, Occurrence, NotionId, ContextId, OccurrenceId. Chacun de ces types est autonome par rapport aux autres crates métier. Tous se sérialisent en JSON. Tous portent une identité stable.

L’enjeu d’implémentation principal est la disjonction des domaines. NotionId et ContextId sont deux types de tuples newtype distincts, chacun encapsulant un Uuid. Cette distinction est centrale : elle empêche le système de types Rust de confondre une notion et un contexte. Avec un seul type Id(Uuid), le compilateur traiterait les deux usages comme équivalents, et la garantie G1 (disjonction 𝓝 ∩ 𝓚 = ∅) deviendrait un vœu plutôt qu’un fait. Le typage par newtype distinct rend la garantie effective.

Atomes premiers — disjonction des domaines par typage 𝓝 notions struct Notion { id: NotionId(Uuid) } 𝓚 contextes struct Context { id: ContextId(Uuid) } 𝓝 ∩ 𝓚 Ω = 𝓝 × 𝓚 struct Occurrence(Notion, Context) OccurrenceId = (NotionId, ContextId) identité dérivée du couple — G2 G1 — Disjonction effective par typage Rust Avec un seul type Id(Uuid), la garantie deviendrait un vœu. Le newtype distinct la rend effective à la compilation.
Figure 3 — La disjonction des domaines 𝓝 et 𝓚 est rendue effective par le typage Rust. Avec un seul type Id(Uuid), la garantie G1 deviendrait un vœu ; avec deux newtype distincts, elle est vérifiée à la compilation.

L’Occurrence est définie comme un couple ordonné (Notion, Context). Son identifiant OccurrenceId est dérivé du couple (NotionId, ContextId), plutôt que généré indépendamment. Cette dérivation matérialise la garantie G2 : une occurrence est constituée par son couple. Deux occurrences instanciées avec les mêmes Notion et Context sous-jacents partagent nécessairement le même OccurrenceId. L’identité de l’occurrence est la conséquence d’une apparition, plutôt qu’un acte de création arbitraire.

La présence d’une méthode Occurrence::signature() qui produit une représentation textuelle lisible (notion@id, context@id) est essentielle. Elle permet à un opérateur humain de lire un système Stratide Flux directement, sans déchiffrer des UUIDs : la signature porte la lisibilité jusque dans la sortie diagnostique.

3.2 Inscription : la relation ▶

Le crate stratide-inscription matérialise la relation primitive d’inscription ▶ ⊆ Ω. Il définit le type générique Inscription<S> paramétré par un Substrate S. La relation d’inscription est portée par un Substrate, qui peut être en mémoire ou persistant.

Cette généricité est un choix d’implémentation important. Elle exprime la régularité G8 : la persistance est optionnelle. Une Inscription<InMemorySubstrate<...>> se comporte comme une inscription transitoire utile pour les tests. Une Inscription<SqliteSubstrate<...>> se comporte comme une inscription persistante utile pour les systèmes en production. Les deux satisfont strictement les mêmes invariants : idempotence, contrôle d’état, sérialisation. L’utilisateur du noyau choisit son support en gardant son code logique stable.

L’inscription est idempotente par construction. Inscrire deux fois la même Occurrence produit une seule entrée, sans déclencher d’erreur, et reste neutre après le premier appel. Cette idempotence est la garantie G5, et elle est testée explicitement dans la suite de tests de chaque substrat. Elle est aussi ce qui permet à l’inscription d’être appelée librement par d’autres modules de manière indépendante : un module qui inscrit une occurrence agit en confiance, qu’un autre module l’ait déjà inscrite ou non.

Inscription ▶ — relation portée par un Substrate Inscription<S> where S: Substrate<()> générique sur le support choix du support Inscription<InMemorySubstrate> Inscription<SqliteSubstrate> G8 — Persistance optionnelle G5 — Idempotence par construction Première inscription : inscription.inscribe(occ_a) stocké · cardinalité=1 Inscription répétée de la même occurrence : inscription.inscribe(occ_a) neutre · cardinalité=1 (inchangée) Nouvelle occurrence : inscription.inscribe(occ_b) stocké · cardinalité=2 L'idempotence permet à plusieurs modules d'inscrire indépendamment, sans coordination préalable.
Figure 4 — L'inscription est portée par un Substrate, qui peut être en mémoire ou persistant. La même Inscription<S> opère identiquement sur les deux supports. L'idempotence est garantie par construction.

3.3 Movement : la relation ◁

Le crate stratide-movement est le pendant du précédent pour la relation primitive . Il définit le type Movement qui matérialise un graphe orienté entre OccurrenceIds. La structure interne est une HashMap<OccurrenceId, HashSet<OccurrenceId>>, ce qui donne une cardinalité de fan-out efficace, le fan-in restant calculé à la demande.

Le choix de garder le fan-in calculable par parcours, plutôt que d’ajouter un index inverse maintenu, a fait l’objet d’une décision explicite documentée dans les limites assumées. L’index inverse serait une optimisation utile pour certaines requêtes, au prix d’une duplication d’information à maintenir cohérente. Tant qu’un cas d’usage l’exige effectivement, l’index reste différé. C’est un choix algorithmique contextuel.

La sérialisation de Movement est un point qui a évolué entre v0.1 et v0.2 et qui mérite une note. La version initiale sérialisait directement la HashMap interne, ce qui exposait au format JSON une représentation où les clés sont nécessairement des strings. La correction, intégrée dès v0.2, sérialise le mouvement comme une liste d’arêtes sous forme [{from, to}, ...]. Cette représentation est stable, ordonnable, transportable. Elle est aussi plus lisible pour un humain qui inspecte le JSON. La désérialisation reconstruit la HashMap interne à partir de la liste d’arêtes. Cette correction illustre une discipline du noyau : la forme sérialisée est stable et lisible, indépendamment des détails internes de représentation.

Movement reste séparé de la notion de Flow qui apparaîtra plus loin. La distinction est centrale et a été formalisée dans la version 0.4.1 : Movement est la relation primitive entre occurrences, Flow est la manifestation contextuelle opérante d’un mouvement dans le cadre d’un Lease. Le premier décrit la structure des mouvements possibles, le second décrit l’exécution effective d’un mouvement. Les deux crates restent séparés parce qu’ils correspondent à deux niveaux conceptuels différents.

Movement (structure) ⊇ Flow (exécution contextuelle) Movement — relation primitive ◁ stratide-movement σ₁ σ₂ σ₃ σ₄ graphe orienté entre OccurrenceIds structure des mouvements possibles « cette occurrence peut se mouvoir vers cette autre » HashMap<OccurrenceId, HashSet<…>> Flow — manifestation opérante stratide-lease-flow Lease<P> cadre d'exécution avec idempotence Flow manifestation contextuelle d'un mouvement Pending Running Completed Failed manifestation contextuelle opérante d'un mouvement dans un Lease « ce mouvement est en cours d'exécution dans ce contexte » cycle protégé · trace persistante
Figure 5 — Movement est la relation primitive entre occurrences (structure des mouvements possibles). Flow est la manifestation contextuelle opérante d'un mouvement dans le cadre d'un Lease (exécution effective). Les deux crates restent séparés parce qu'ils correspondent à deux niveaux conceptuels distincts.

Section 4 — Infrastructure des inscriptions

La deuxième famille de modules constitue l’infrastructure technique sur laquelle repose le régime. Un seul crate s’y consacre — stratide-substrate-provider — et ce crate est de loin le plus volumineux du noyau, parce qu’il porte à la fois la définition des contrats abstraits et plusieurs implémentations concrètes.

4.1 La hiérarchie des traits Substrate

Le crate définit cinq traits, organisés en hiérarchie :

Substrate<T>                          (trait commun minimal)
    ├── RelationalSubstrate<T>        ✓ in-memory + SQLite
    ├── DocumentSubstrate<T>          (interface posée, implémentation différée)
    ├── EventSubstrate<T>             ✓ in-memory + SQLite (depuis v0.4)
    └── FileSubstrate<T>              (interface posée, implémentation différée)

Le trait Substrate<T> est commun à tous les sous-types. Il définit les opérations minimales : store, retrieve, delete, contains, cardinality, clear, plus la gestion des états. Tout substrat, quel que soit son mode d’inscription, satisfait ces opérations.

Les sous-traits ajoutent les spécificités. RelationalSubstrate<T> ajoute list_keys, list_all, insert_if_absent : opérations naturelles d’un store relationnel. EventSubstrate<T> ajoute un type associé Offset, et trois méthodes de lecture par offset : latest_offset, read_from, read_initial. La sémantique d’écriture y est append-only : appeler store(key, value) ajoute toujours un nouvel événement à la fin du journal, indépendamment de l’existence d’une valeur sous la même clé. DocumentSubstrate<T> ajoute query_by_field (recherche par chemin de champ JSON). FileSubstrate<T> ajoute la notion de version avec store_versioned, retrieve_version, list_versions.

La présence des quatre sous-traits dans le code, dont deux à implémentation différée, est volontaire. Elle est l’expression de la typologie complète des modes d’inscription que reconnaît la théorie. Le noyau reconnaît la typologie complète, et matérialise les implémentations selon les cas d’usage qui les exigent. La présence des quatre traits affirme la complétude conceptuelle ; la livraison progressive des implémentations affirme l’économie de complexité.

Hiérarchie des traits Substrate — typologie complète Substrate<T> store · retrieve · delete · contains cardinality · clear · états contextuels RelationalSubstrate list_keys · list_all insert_if_absent ✓ implémenté in-memory + SQLite DocumentSubstrate query_by_field recherche par chemin ⋯ contrat posé implémentation différée EventSubstrate latest_offset · read_from append-only par offset ✓ implémenté (v0.4) in-memory + SQLite FileSubstrate store_versioned retrieve_version · list ⋯ contrat posé implémentation différée Implémentations concrètes InMemorySubstrate SqliteSubstrate InMemoryEventSubstrate SqliteEventSubstrate Pourquoi les quatre traits, dont deux à implémentation différée ? La présence des quatre affirme la complétude conceptuelle de la typologie. La livraison progressive des implémentations affirme l'économie de complexité.
Figure 6 — La hiérarchie des traits Substrate exprime la typologie complète des modes d'inscription. Quatre traits sont posés ; deux ont leurs implémentations concrètes (relationnel et événementiel), deux gardent leur implémentation différée (documentaire et fichier).

4.2 Les états contextuels du Substrat

Tout Substrat porte un état contextuel parmi quatre : Manifest, Inactive, Dormant, Awakening. Cet état contrôle l’admissibilité des opérations. En Manifest, lecture et écriture sont autorisées. En Inactive, la lecture est autorisée. En Dormant, le substrat est suspendu. En Awakening, la lecture est autorisée comme un état de transition vers le retour à Manifest.

Les transitions entre états sont contrôlées par une fonction can_transition_to qui implémente une matrice de transitions admissibles. Chaque transition est qualifiée explicitement. Par exemple, la transition de Manifest à Awakening exige le passage préalable par Dormant. Cette discipline matérialise la régularité Rg-G1 de réversibilité par dormance que pose la théorie : un substrat est suspendu plutôt que détruit, et son réveil est une opération distincte du retour à la manifestation.

Le contrôle d’état est appliqué uniformément dans toutes les opérations de tous les substrats. Une tentative d’écriture sur un substrat dormant lève une SubstrateError::InvalidState avec un message clair. Cette rigueur garantit que le contexte de manifestation reste opératoire : il est observable et il est contraignant.

États contextuels du Substrat — réversibilité par dormance (Rg-G1) Manifest lecture · écriture opérations admises Inactive lecture seule écriture suspendue Dormant substrat suspendu opérations refusées Awakening lecture autorisée transition vers Manifest deactivate activate put_dormant put_dormant awaken manifest Transition Manifest → Awakening : passage préalable par Dormant Un substrat est suspendu plutôt que détruit. Son réveil est une opération distincte du retour à la manifestation.
Figure 7 — Tout substrat porte un état contextuel parmi quatre. Les transitions sont qualifiées explicitement par une matrice ; le passage de Manifest à Awakening exige un détour par Dormant. Cette discipline matérialise la régularité Rg-G1.

4.3 Les implémentations concrètes

Le crate livre quatre implémentations concrètes :

InMemorySubstrate<T, K> est un substrat relationnel en mémoire. Il s’appuie sur une HashMap protégée par un RwLock (du crate parking_lot, choisi pour sa performance et sa simplicité par rapport au RwLock de la bibliothèque standard). Il est le substrat par défaut pour tests et prototypes.

SqliteSubstrate<T> est un substrat relationnel persistant adossé à SQLite via le crate rusqlite en mode bundled. La table sous-jacente comporte une clé composée (key_notion, key_context) et une colonne value qui sérialise la valeur en JSON. La connexion est protégée par un Mutex plutôt qu’un RwLock parce que SQLite sérialise les accès via la même connexion.

InMemoryEventSubstrate<T> est un journal d’événements en mémoire. Sa structure interne est une Vec<(u64, OccurrenceId, T)> avec un offset croissant maintenu séparément. Le mode est append-only : store ajoute toujours.

SqliteEventSubstrate<T> est le journal d’événements persistant. La table SQLite est configurée avec offset INTEGER PRIMARY KEY AUTOINCREMENT qui garantit l’unicité et la croissance des offsets, gérée directement par le moteur SQLite.

Une note d’implémentation mérite mention. La méthode contains du substrat SQLite a fait l’objet d’une correction explicite pendant la livraison v0.4 : il s’est avéré nécessaire de forcer la destruction du Statement SQLite avant la fin de portée du lock, afin de garantir l’ordre entre la libération du verrou et la destruction du statement. Cette correction est typique de la matérialisation : ce qui est conceptuellement simple peut nécessiter des précautions techniques précises, et le rôle d’une cartographie post-livraison est d’identifier ces précautions pour qu’elles soient consignées.

4.4 Le Provider composé

Au-dessus des Substrates, le crate définit un Provider composé qui matérialise la triade Engine × Function × Substrate. Un Engine décrit le moteur technique (InMemoryEngine, SqlEngine). Une Function décrit la sémantique d’inscription (CrudFunction est l’implémentation concrète actuelle). Un Substrate porte les inscriptions matérielles. Le Provider compose les trois et expose des méthodes inscribe, fetch, uninscribe qui vérifient l’état de l’engine et les capacités de la function avant de déléguer au substrate.

Cette composition est ce qui permet d’exprimer en code la décision méthodologique de la section 3.2 de l’article fondateur : engine et function sont distincts, leur combinaison définit la sémantique opérante d’un Provider. Une combinaison incompatible (par exemple une function en lecture seule sollicitée pour une écriture) est détectée à l’appel et signalée explicitement.

Provider — composition Engine × Function × Substrate Engine moteur technique InMemoryEngine SqlEngine avec contrôle d'état × Function sémantique d'inscription CrudFunction capabilities : read · create · update · delete × Substrate support des inscriptions InMemorySubstrate SqliteSubstrate …EventSubstrate Provider<E, F, S, T> composition vérifiée à l'appel .inscribe(notion, value) .fetch(notion) · .uninscribe(notion) Vérifications avant délégation • État de l'engine (Manifest / Inactive…) • Capabilities de la function (read/write…) • Cohérence engine ↔ substrate Combinaison incompatible Function en lecture seule + écriture demandée → ProviderError signalée à l'appel avec message explicite
Figure 8 — Le Provider compose la triade Engine × Function × Substrate. Avant de déléguer, il vérifie l'état de l'engine, les capabilities de la function et la cohérence avec le substrate. Une combinaison incompatible est signalée explicitement.

Section 5 — Manifestations contextuelles

La troisième famille de modules porte les manifestations contextuelles : les centres stabilisés, les contracts, les leases, les flows, les traces. Deux crates s’y consacrent — stratide-endpoint-field et stratide-lease-flow — et ils représentent ensemble la moitié supérieure du noyau, là où la théorie déploie sa puissance pratique.

5.1 Endpoints et champs

Le crate stratide-endpoint-field matérialise la notion de centre stabilisé. Un Endpoint est défini par un nom, une Occurrence sous-jacente, un EndpointId distinct de l’OccurrenceId, un état dans son cycle de vie, et une évaluation de lisibilité.

La distinction entre EndpointId et OccurrenceId est un choix de modélisation important. Un Endpoint est un centre stabilisé construit à partir d’une Occurrence ; il a sa propre identité. Plusieurs Endpoints différents peuvent être construits à partir de la même Occurrence sous-jacente (par exemple : un endpoint de lecture et un endpoint d’écriture pour la même notion-contexte). Leur identité propre garantit la garantie G11.

L’évaluation de lisibilité est portée par le type ReadabilityAssessment, qui agrège trois conditions : meaning, reachability, understanding. Chaque condition est un ReadabilityCondition avec un booléen satisfied et un détail textuel. L’évaluation est graduelle : un Endpoint peut être pleinement lisible (les trois conditions satisfaites), partiellement lisible (au moins une parmi les trois) ou en attente d’évaluation. Cette graduation matérialise la garantie G12. Un Endpoint partiellement lisible peut être activé, ce qui est volontaire : la théorie pose la lisibilité comme observable et graduée, plutôt que comme un seuil binaire préalable à l’opération.

Le calcul du champ ◇_D(σ) d’un Endpoint est confié à la fonction Field::compute(center, direction, &inscription, &movement). Le calcul est lazy : il est effectué à la demande, en parcourant les Occurrences inscrites et en filtrant celles vers lesquelles le centre se meut directement. La complexité est en O(n) sur le nombre d’occurrences inscrites. Cette simplicité algorithmique est délibérée : le calcul de champ est un outil de diagnostic et de modélisation, employé en dehors des boucles chaudes. Un cas d’usage exigeant des champs calculés en haute fréquence justifierait l’ajout d’un index en extension conditionnelle.

Le Field::compute exige un substrat relationnel (RelationalSubstrate<()>) plutôt que le simple substrat commun. Cette exigence a été identifiée pendant la livraison v0.3 et intégrée dans la signature : le calcul du champ s’appuie sur la capacité de lister les clés inscrites. Cette contrainte est typique de l’évolution d’un noyau par paliers : ce qui devient nécessaire à un palier donné est intégré à ce moment-là, et la signature évolue avec lui.

L’EndpointRegistry indexe les endpoints par EndpointId et par OccurrenceId. Sa sérialisation est une liste stable d’endpoints, plutôt qu’une représentation de la HashMap interne. Cette discipline de sérialisation a été établie dès Movement et reprise pour tous les registres : l’index interne est une optimisation de lecture, l’inventaire stable est la forme transportable.

Endpoint — centre stabilisé, lisibilité graduelle, champ ◇_D(σ) Endpoint centre stabilisé EndpointId propre · UUID v4 identité du centre OccurrenceId sous-jacent notion@contexte l'apparition portée G11 — identités distinctes ReadabilityAssessment — évaluation graduelle (G12) meaning condition de sens satisfied: bool · detail reachability condition d'atteignabilité satisfied: bool · detail understanding condition de compréhension satisfied: bool · detail Field::compute(c, d, &ins, &mov) calcul lazy O(n) — outil de diagnostic ◇_D(σ) ⊆ Ω occurrences atteignables depuis σ via direction D
Figure 9 — Un Endpoint est un centre stabilisé construit à partir d'une Occurrence ; il a sa propre identité (G11). Sa lisibilité est évaluée graduellement sur trois conditions (G12). Le calcul du champ ◇_D(σ) est lazy et destiné au diagnostic.

5.2 Contracts

Le crate stratide-lease-flow matérialise la spécification orientée des mouvements via le type Contract. Un Contract relie deux EndpointIds (source et sink), porte un nom, une version, un état dans son cycle de vie, et optionnellement une description.

Une décision de design centrale a été prise pour la version 0.4 : le Contract est neutre par rapport au type de payload. Il reste un type concret unique, sans paramètre générique. La justification est double. D’une part, le Contract spécifie ce qui peut transiter ; son rôle est descriptif. D’autre part, un Contract neutre permet d’enregistrer hétérogènement plusieurs Contracts dans un même ContractRegistry en gardant le registre lui-même simple. C’est la garantie G15.

Le typage du payload est porté par les Leases qui instancient le Contract. Cette séparation permet à un même Contract de coexister avec plusieurs versions de payload (par exemple lors d’une transition de version), en gardant le noyau ouvert au détail métier.

Le Contract porte un état parmi quatre : Draft, Active, InTransition, Retired. La transition Active → InTransition est utilisée lors d’un changement de version où l’ancien Contract et le nouveau coexistent pendant une période de migration. Pendant cette période, les deux acceptent de nouveaux Leases, ce qui est exprimé par la méthode accepts_new_leases() qui retourne true pour Active ou InTransition et false pour Draft ou Retired. Un Contract retiré est définitivement clos : seuls les Leases déjà ouverts continuent leur cycle de vie.

Contract neutre + Lease<P> générique — séparation spec/instance Contract G15 — neutre par rapport au payload id: ContractId name · version source: EndpointId sink: EndpointId state: ContractState ContractState — 4 états Draft Active ✓ InTransition Retired accepts_new_leases() : Active · InTransition → true instancie via Lease Lease<P> G16 — paramétré par le payload id: LeaseId contract: ContractId payload: Option<P> idempotency_key: … state: LeaseState created_at: u64 IdempotencyKey — dual mode FromLease(LeaseId) par défaut · unicité UUID Explicit(String) sémantique métier · "commande-12345" .with_idempotency_key("…")
Figure 10 — Le Contract est neutre par rapport au payload (G15). Le typage du payload est porté par les Lease<P> qui l'instancient (G16). La clé d'idempotence du Lease accepte deux modes : dérivée du LeaseId par défaut, ou explicite quand le client veut une sémantique métier.

5.3 Leases et idempotence

Le Lease<P> est le type le plus dense conceptuellement. Il est paramétré par le type de payload P. Il porte un LeaseId, le ContractId du Contract instancié, un état dans son cycle de vie, une clé d’idempotence, un payload optionnel, et un timestamp de création.

La clé d’idempotence est l’élément le plus délicat. Le type IdempotencyKey est un enum à deux variantes :

IdempotencyKey::FromLease(LeaseId)      // dérivée du LeaseId, par défaut
IdempotencyKey::Explicit(String)        // fournie par le client

Cette dualité est un choix explicite documenté. Le mode par défaut (FromLease) suffit pour la majorité des cas d’usage : le LeaseId étant un UUID, il garantit l’unicité et donc l’anti-replay. Le mode explicite est utile lorsque le client veut une idempotence sémantique métier, par exemple un numéro de commande, un identifiant externe, une clé fonctionnelle. La méthode fluent with_idempotency_key("commande-12345") permet de basculer du mode par défaut au mode explicite. C’est la garantie G16.

Le cycle de vie d’un Lease comporte quatre états : Active, Dormant, Terminated, Failed. La transition Active → Dormant correspond à une suspension contextuelle (par exemple : attente d’une condition externe). La transition Dormant → Active est un réveil. La transition Active → Terminated est une terminaison normale. La transition Active → Failed est une terminaison sur erreur. La méthode awaken() est volontairement protégée : elle réveille un Lease uniquement depuis l’état Dormant. Pour tout autre état (notamment Terminated ou Failed), awaken() reste neutre. Cette protection matérialise la régularité Rg-G1 de manière stricte : le réveil est une transition possible depuis la dormance, et il préserve les états terminaux.

Le timestamp de création (created_at) est calculé par SystemTime::now() au moment de la construction du Lease, exprimé en secondes depuis l’époque Unix. Cette information sert aux opérations de tri et d’inventaire sur les Leases directement, indépendamment d’un journal externe (l’observabilité applicative passe par les Traces).

Cycle de vie d'un Lease — réversibilité Active ↔ Dormant Active cadre opérant Flows possibles Dormant suspension contextuelle attente de condition Terminated terminaison normale ▪ état terminal Failed terminaison sur erreur ▪ état terminal .suspend() .awaken() .complete() .fail() awaken() depuis Terminated/Failed → reste neutre Rg-G1 — réversibilité par dormance, terminaisons préservées Le réveil est une transition possible depuis la dormance ; il préserve les états terminaux.
Figure 11 — Le cycle de vie d'un Lease comporte deux états réversibles (Active ↔ Dormant) et deux états terminaux (Terminated, Failed). La méthode awaken() préserve les états terminaux : appelée sur un Lease terminé, elle reste neutre.

5.4 Flows et idempotence héritée

Le Flow est la manifestation primaire du mouvement. Il porte un FlowId, le LeaseId du Lease auquel il se rapporte, une clé d’idempotence, et un état dans son cycle de vie (Pending, Running, Completed, Failed).

La construction d’un Flow exige explicitement la IdempotencyKey du Lease : Flow::for_lease(lease_id, idempotency_key). Cette signature matérialise la garantie G17 : la chaîne d’idempotence reste continue entre Lease et Flow. Deux Flows partageant la même clé d’idempotence correspondent conceptuellement à la même opération métier, et le noyau le rend lisible par la signature.

Le cycle d’un Flow est protégé. La méthode start() fait passer à Running uniquement depuis Pending. La méthode complete() fait passer à Completed uniquement depuis Running. La méthode fail() accepte la transition depuis Pending ou Running, et préserve l’état Completed (un Flow terminé avec succès reste terminé avec succès). Ces gardes maintiennent l’observabilité du Flow claire et déterministe.

Chaîne d'idempotence — Lease & Flow (G17) Lease<Order> id: ls:e7b9-… key: "commande-12345" Flow::for_lease(lease_id, key) Flow #1 id: fl:a1b2-… lease: ls:e7b9-… key: "commande-12345" tentative initiale Flow #2 id: fl:c3d4-… lease: ls:e7b9-… key: "commande-12345" retry après timeout Flow #3 id: fl:e5f6-… lease: ls:e7b9-… key: "commande-12345" retry final Trois Flows · une seule clé d'idempotence · une seule opération métier L'application en aval voit la clé identique et déduit que les trois Flows correspondent au même acte. Anti-replay garanti par la signature Flow::for_lease. G17 — La chaîne d'idempotence reste continue entre Lease et Flow ; la signature la rend lisible.
Figure 12 — La signature Flow::for_lease(lease_id, idempotency_key) maintient la chaîne d'idempotence entre Lease et Flow (G17). Plusieurs Flows partageant la même clé correspondent conceptuellement à la même opération métier.

5.5 Traces

Les Traces consignent les manifestations effectives. Le noyau a vu deux types de traces se succéder, et la version 0.4.1 les fait coexister.

Le type Trace (v0.4) est simple : il porte un LeaseId, un TraceKind (Created, StateChanged, Completed, Failed, Awakened), un timestamp et un détail textuel. Il est consigné dans un EventSubstrate via un TraceRecorder<S> générique sur le substrat. Le TraceRecorder construit une OccurrenceId synthétique à partir de la trace pour la stocker dans le journal.

Le type EnrichedTrace (v0.4.1) ajoute trois capacités. Un identifiant propre TraceId qui distingue la trace de son support de stockage. Un CorrelationId qui permet de regrouper plusieurs traces appartenant à la même opération métier (par exemple toutes les traces générées par l’ouverture, l’exécution et la complétion d’un Lease). Un causation_id: Option<TraceId> qui établit explicitement la chaîne causale : trace B causée par trace A.

Le TraceEntity du EnrichedTrace est typé : il pointe vers une Lease(LeaseId), un Flow(FlowId), un Contract(ContractId), un Endpoint(EndpointId) ou une Occurrence(OccurrenceId). Ce typage exhaustif permet à l’EnrichedTraceRecorder de proposer des requêtes de filtrage par entité ou par corrélation, et de reconstruire la chaîne causale via causal_chain(trace_id, limit).

La coexistence des deux types est une garantie additionnelle (G19). Le code applicatif qui utilisait Trace en v0.4 continue de fonctionner en v0.4.1. Le code nouveau peut utiliser EnrichedTrace quand les capacités de corrélation sont nécessaires. La migration entre les deux types reste à la main de l’utilisateur. Cette coexistence est typique du régime de progression du noyau : on ajoute aux côtés de l’existant.

Coexistence Trace / EnrichedTrace + chaîne causale (G19) Trace (v0.4) simple consignation par Lease struct Trace { lease: LeaseId, kind: TraceKind, timestamp: u64, detail: String, } EnrichedTrace (v0.4.1) corrélation + chaîne causale struct EnrichedTrace { id: TraceId, correlation: CorrelationId, causation: Option<TraceId>, entity: TraceEntity, kind, timestamp, detail } Chaîne causale d'une opération métier CorrelationId = co:7f3a-… (toutes les traces partagent la même) T1 — Lease Created id: tr:001… entity: Lease(ls:e7b9) causation: None T2 — Flow Created id: tr:002… entity: Flow(fl:a1b2) causation: tr:001 T3 — Flow Completed id: tr:003… entity: Flow(fl:a1b2) causation: tr:002 cause cause EnrichedTraceRecorder.causal_chain(tr:003) → [T3, T2, T1] remontée transitive jusqu'à la trace racine (causation: None) Le code v0.4 utilisant Trace continue de fonctionner. Le code nouveau choisit EnrichedTrace quand la corrélation et la causalité apportent une valeur observable.
Figure 13 — La v0.4.1 fait coexister Trace (v0.4) et EnrichedTrace (G19). EnrichedTrace ajoute identité propre, corrélation et causation explicite. La méthode causal_chain reconstruit la séquence causale par remontée transitive.

5.6 Le trait DormantLifecycle

La version 0.4.1 a également introduit un trait DormantLifecycle qui formalise la régularité Rg-G1 au niveau du système de types. Le trait définit quatre méthodes : id(), is_dormant(), dormant(), awaken(), plus un type associé Id.

Le trait reste sans implémentation imposée sur les types existants (Endpoint, Contract, Lease). Cette décision est volontaire et documentée comme garantie G20. Implémenter le trait sur les types de v0.4 aurait exigé d’aligner leurs signatures, ce qui aurait modifié des API stables. Le trait existe pour permettre l’écriture de fonctions génériques sur la dormance, et il sera implémenté par les types nouveaux de la version 0.5 (notamment Project) où l’homogénéité des signatures peut être imposée par construction.

Cette manière d’introduire un trait en gardant son adoption progressive est typique d’un palier de préparation : le contrat est posé, et son adoption suit le rythme des types qui peuvent l’implémenter sans rupture.


Section 6 — Posture finale et perspectives

Cet article a parcouru la matérialisation du noyau Stratide Flux à la version 0.4.1. Il a documenté les sept crates, leur stratification, leurs garanties (au nombre de vingt à ce jour), leurs choix d’implémentation et leurs limites assumées. Il a expliqué comment chaque décision technique reflète une décision théorique, et comment chaque limite est soit un choix structurel permanent, soit un report volontaire vers un palier ultérieur.

Trois éléments situent ce que ce noyau est aujourd’hui.

Premièrement, le noyau est une bibliothèque embeddable. Il fournit les types, les traits, les implémentations qui permettent à un produit d’incarner Stratide Flux. Le franchissement de la frontière entre noyau et produit est une décision distincte, prise dans le cadre de Stratide Order, le produit applicatif construit au-dessus du noyau. Stratide Order incorporera du protocole, des interfaces, des intégrations métier. Le noyau, lui, reste ce qu’il est : un ensemble de bibliothèques Rust embeddables, exposables par n’importe quel produit selon ses propres choix.

Deuxièmement, le noyau est à six modules sur sept à la version 0.4.1. Le septième et dernier module, ContractApi-Project, portera la gouvernance globale et locale. Ce module est planifié pour la version 0.5. Il introduira les types Project (centre d’autorité locale), ContractApi (gardien universel), ContractScope (qualification intra/inter-Project), et les politiques de coordination inter-Projects. Sa livraison fermera structurellement le noyau, et la version 1.0 qui suivra stabilisera les API publiques. La présente cartographie sera donc révisée une fois en v0.5, puis figée en v1.0.

Troisièmement, deux sous-traits gardent leur implémentation différée jusqu’à un déclencheur métier. DocumentSubstrate et FileSubstrate sont posés comme contrats d’interface, et leur implémentation viendra avec un cas d’usage métier réel qui en justifie le coût. Cette discipline est l’expression d’un principe d’économie de complexité : implémenter un store concret demande de figer des choix techniques (MongoDB, JSONB PostgreSQL, store custom), et ces choix méritent d’être pris à la lumière d’un cas d’usage qui les valide. Ces implémentations viendront probablement dans le contexte de Stratide Order plutôt que dans le noyau lui-même.

Le statut de cet article est lui aussi à situer. Il documente une matérialisation, plutôt qu’il n’en prescrit une. Une autre implémentation, dans un autre langage, ferait probablement d’autres choix techniques. L’invariant à préserver est le régime architectural : la stratification en sept modules, la disjonction des identités, la séparation entre Movement et Flow, la neutralité des Contracts par rapport au payload, l’idempotence des Flows par clé, la persistance optionnelle, la dormance comme régime de réversibilité.

Les vingt garanties matérialisées à ce jour (G1 à G18 en v0.4, G19 et G20 en v0.4.1) sont des engagements que le noyau prend vis-à-vis de ses utilisateurs : voici ce qui est vrai en Stratide Flux indépendamment du contexte d’usage. La cartographie post-livraison de chaque palier vérifie ces engagements et les documente. Cette vérification continue est ce qui permet au noyau de rester lisible à la lecture comme à l’audit.

Le noyau est aujourd’hui à six modules sur sept. Le palier v0.4.1 a préparé le terrain pour le septième, en posant l’infrastructure de corrélation des traces (EnrichedTrace) et le contrat de dormance (DormantLifecycle). Reste à introduire la gouvernance, à formaliser les garanties G21 et au-delà, à clore le noyau et à le stabiliser. Cette dernière étape est un engagement : la version 1.0 figera les API publiques, et toute évolution ultérieure respectera la rétro-compatibilité.

Au-delà du noyau, Stratide Order ouvrira une autre histoire : celle de l’incarnation produit. Cette histoire mérite ses propres articles, et elle commencera quand la version 1.0 du noyau sera livrée. D’ici là, le noyau continue sa trajectoire de paliers, avec la même discipline : un module à la fois, une cartographie après chaque livraison, et l’invariant maintenu d’une frontière nette entre théorie, architecture, matérialisation et produit.

Trajectoire du noyau — paliers livrés et à venir v0.1 Notion·Context Inscription·Movement G1-G7 v0.2 Substrate-Provider SQLite +G8-G10 v0.3 Endpoint-Field lisibilité, champs +G11-G14 v0.4 Lease-Flow Contract·Lease·Flow +G15-G18 v0.4.1 EnrichedTrace DormantLifecycle +G19-G20 ◀ ICI v0.5 ContractApi-Project Project·gouvernance +G21+ v1.0 stabilisation API publiques figées Stratide Flux — noyau Bibliothèques Rust embeddables 7 crates · 20 garanties à v0.4.1 v1.0 ferme structurellement le noyau Stratide Order — produit Incarnation produit du noyau Protocole · interfaces · métier démarre après v1.0 du noyau Frontière maintenue : noyau (Rust embeddable) ≠ produit (interfaces, intégrations)
Figure 14 — Trajectoire des paliers du noyau. Six modules sur sept livrés à la version 0.4.1 ; le septième (ContractApi-Project) est planifié pour v0.5 et fermera structurellement le noyau. La version 1.0 stabilisera les API publiques. Au-delà commencera Stratide Order.

Document de référence — Stratide Flux v0.4.1 — © 2026 Herisolo Rabosaona Compagnon de l’article fondateur. À lire après lui, pour comprendre comment le régime architectural devient code. Site Stratide : https://stratide.org