Stratide Flux — Implémentation
Matérialisation Rust du noyau v0.4.1
Du régime architectural au code : sept modules, vingt garanties, quatorze schémas
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.
À 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)
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.
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.
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.
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é.
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.
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.
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.
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.
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).
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.
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.
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.
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