diff --git a/Cargo.toml b/Cargo.toml index 73cbc53..dd7ae7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 86 +# version: 87 [workspace] resolver = "3" @@ -13,6 +13,7 @@ members = [ "crates/apps/game-snake-poc-desktop", "crates/common/game-assets-lib", "crates/common/game-logging-lib", + "crates/common/game-realtime-transport-lib", "crates/apps/game-android-entrypoint", "crates/apps/game-reflex-poc-tauri", "crates/apps/game-reflex-poc-wasm", @@ -21,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.3.4-alpha.1" +version = "0.3.4-alpha.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index c1442fe..0db83c2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # games.sasedev @@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale Version stable de référence : `0.3.3`. -Version candidate active : `0.3.4-alpha.1`. `0.3.2` reste différée. +Version candidate active : `0.3.4-alpha.2`. `0.3.2` reste différée. -La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.1` migre la nomenclature de prerelease et cadre l'API de transport realtime/WebSocket avant toute implémentation réseau lourde. +La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.2` introduit la première API transport-neutral realtime : messages binaires opaques, fermeture distante explicite, erreurs catégorisées et split statique sender/receiver, sans dépendance Tokio ou WebSocket. Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme. diff --git a/crates/common/game-realtime-transport-lib/Cargo.toml b/crates/common/game-realtime-transport-lib/Cargo.toml new file mode 100644 index 0000000..998bd92 --- /dev/null +++ b/crates/common/game-realtime-transport-lib/Cargo.toml @@ -0,0 +1,14 @@ +# file: crates/common/game-realtime-transport-lib/Cargo.toml +# version: 1 + +[package] +name = "game-realtime-transport-lib" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[lints] +workspace = true diff --git a/crates/common/game-realtime-transport-lib/src/connection.rs b/crates/common/game-realtime-transport-lib/src/connection.rs new file mode 100644 index 0000000..469fb90 --- /dev/null +++ b/crates/common/game-realtime-transport-lib/src/connection.rs @@ -0,0 +1,42 @@ +// file: crates/common/game-realtime-transport-lib/src/connection.rs +// version: 1 + +/// Established realtime connection that owns independent send and receive halves. +pub trait RealtimeConnection { + /// Send half produced when this connection is split. + type Sender: crate::RealtimeSender; + /// Receive half produced when this connection is split. + type Receiver: crate::RealtimeReceiver; + + /// Consumes the connection and returns independent send and receive halves. + fn split(self) -> (Self::Sender, Self::Receiver); +} + +/// Send half of an established realtime connection. +pub trait RealtimeSender { + /// Future returned by [`RealtimeSender::send`]. + type SendFuture<'a>: core::future::Future> + where + Self: 'a; + /// Future returned by [`RealtimeSender::close`]. + type CloseFuture<'a>: core::future::Future> + where + Self: 'a; + + /// Sends one owned opaque binary message, respecting backend backpressure. + fn send(&mut self, message: crate::TransportMessage) -> Self::SendFuture<'_>; + + /// Initiates a clean local transport-level close. + fn close(&mut self) -> Self::CloseFuture<'_>; +} + +/// Receive half of an established realtime connection. +pub trait RealtimeReceiver { + /// Future returned by [`RealtimeReceiver::receive`]. + type ReceiveFuture<'a>: core::future::Future> + where + Self: 'a; + + /// Receives one binary message or observes a clean remote close. + fn receive(&mut self) -> Self::ReceiveFuture<'_>; +} diff --git a/crates/common/game-realtime-transport-lib/src/error.rs b/crates/common/game-realtime-transport-lib/src/error.rs new file mode 100644 index 0000000..65c3bdd --- /dev/null +++ b/crates/common/game-realtime-transport-lib/src/error.rs @@ -0,0 +1,82 @@ +// file: crates/common/game-realtime-transport-lib/src/error.rs +// version: 1 + +/// Stable transport-neutral category for a realtime operation failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TransportErrorKind { + /// An endpoint or transport configuration is invalid. + InvalidConfiguration, + /// A client connection could not be established. + Connect, + /// A server endpoint could not be bound. + Bind, + /// A server could not accept an incoming connection. + Accept, + /// An operation exceeded its configured deadline. + Timeout, + /// A payload exceeds the configured transport bound. + MessageTooLarge, + /// The transport cannot currently accept more outbound data within its configured bounds. + Backpressure, + /// The requested operation requires a connection that is already closed. + Closed, + /// An underlying I/O operation failed. + Io, + /// The backend reported a protocol-level or transport-specific failure. + Protocol, + /// The operation was explicitly aborted or cancelled when that distinction is observable. + Aborted, +} + +impl core::fmt::Display for TransportErrorKind { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + return match self { + crate::TransportErrorKind::InvalidConfiguration => formatter.write_str("invalid configuration"), + crate::TransportErrorKind::Connect => formatter.write_str("connect failure"), + crate::TransportErrorKind::Bind => formatter.write_str("bind failure"), + crate::TransportErrorKind::Accept => formatter.write_str("accept failure"), + crate::TransportErrorKind::Timeout => formatter.write_str("operation timed out"), + crate::TransportErrorKind::MessageTooLarge => formatter.write_str("message too large"), + crate::TransportErrorKind::Backpressure => formatter.write_str("transport backpressure"), + crate::TransportErrorKind::Closed => formatter.write_str("connection closed"), + crate::TransportErrorKind::Io => formatter.write_str("I/O failure"), + crate::TransportErrorKind::Protocol => formatter.write_str("protocol failure"), + crate::TransportErrorKind::Aborted => formatter.write_str("operation aborted"), + }; + } +} + +/// Transport-neutral operation failure with a stable category and diagnostic detail. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransportError { + detail: String, + kind: crate::TransportErrorKind, +} + +impl TransportError { + /// Creates a transport failure from a stable category and backend-neutral diagnostic detail. + #[must_use] + pub fn new(kind: crate::TransportErrorKind, detail: impl Into) -> Self { + return Self { detail: detail.into(), kind }; + } + + /// Borrows the diagnostic detail associated with the failure. + #[must_use] + pub fn detail(&self) -> &str { + return self.detail.as_str(); + } + + /// Returns the stable transport-neutral failure category. + #[must_use] + pub fn kind(&self) -> crate::TransportErrorKind { + return self.kind; + } +} + +impl core::fmt::Display for TransportError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + return write!(formatter, "{}: {}", self.kind, self.detail); + } +} + +impl std::error::Error for TransportError {} diff --git a/crates/common/game-realtime-transport-lib/src/lib.rs b/crates/common/game-realtime-transport-lib/src/lib.rs new file mode 100644 index 0000000..75ebe41 --- /dev/null +++ b/crates/common/game-realtime-transport-lib/src/lib.rs @@ -0,0 +1,31 @@ +// file: crates/common/game-realtime-transport-lib/src/lib.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Transport-neutral realtime connection contracts for games.sasedev. + +mod connection; +mod error; +mod message; + +/// Re-export of an established realtime connection that can be split into independent halves. +pub use self::connection::RealtimeConnection; +/// Re-export of the receive half of an established realtime connection. +pub use self::connection::RealtimeReceiver; +/// Re-export of the send half of an established realtime connection. +pub use self::connection::RealtimeSender; +/// Re-export of a transport-neutral operation failure. +pub use self::error::TransportError; +/// Re-export of transport-neutral error categories. +pub use self::error::TransportErrorKind; +/// Re-export of an owned opaque binary transport message. +pub use self::message::TransportMessage; +/// Re-export of the result of one receive operation. +pub use self::message::TransportReceive; + +#[cfg(test)] +#[path = "../unit_tests/contract.rs"] +mod tests; diff --git a/crates/common/game-realtime-transport-lib/src/message.rs b/crates/common/game-realtime-transport-lib/src/message.rs new file mode 100644 index 0000000..bbcd684 --- /dev/null +++ b/crates/common/game-realtime-transport-lib/src/message.rs @@ -0,0 +1,49 @@ +// file: crates/common/game-realtime-transport-lib/src/message.rs +// version: 1 + +/// Owned opaque binary payload carried by a realtime transport. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransportMessage { + bytes: Vec, +} + +impl TransportMessage { + /// Creates a transport message from owned bytes. + #[must_use] + pub fn new(bytes: Vec) -> Self { + return Self { bytes }; + } + + /// Borrows the opaque payload bytes. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + return self.bytes.as_slice(); + } + + /// Consumes the message and returns its owned payload bytes. + #[must_use] + pub fn into_bytes(self) -> Vec { + return self.bytes; + } + + /// Returns whether the payload is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + return self.bytes.is_empty(); + } + + /// Returns the payload length in bytes. + #[must_use] + pub fn len(&self) -> usize { + return self.bytes.len(); + } +} + +/// Outcome of one successful transport receive operation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TransportReceive { + /// One opaque binary message was received. + Message(crate::TransportMessage), + /// The remote peer completed a clean transport-level close. + Closed, +} diff --git a/crates/common/game-realtime-transport-lib/unit_tests/contract.rs b/crates/common/game-realtime-transport-lib/unit_tests/contract.rs new file mode 100644 index 0000000..672e4b0 --- /dev/null +++ b/crates/common/game-realtime-transport-lib/unit_tests/contract.rs @@ -0,0 +1,131 @@ +// file: crates/common/game-realtime-transport-lib/unit_tests/contract.rs +// version: 1 + +#[derive(Debug, Eq, PartialEq)] +struct TestSender { + closed: bool, + sent_bytes: usize, +} + +impl crate::RealtimeSender for TestSender { + type SendFuture<'a> + = std::future::Ready> + where + Self: 'a; + type CloseFuture<'a> + = std::future::Ready> + where + Self: 'a; + + fn send(&mut self, message: crate::TransportMessage) -> Self::SendFuture<'_> { + self.sent_bytes = self.sent_bytes.saturating_add(message.len()); + return std::future::ready(Ok(())); + } + + fn close(&mut self) -> Self::CloseFuture<'_> { + self.closed = true; + return std::future::ready(Ok(())); + } +} + +#[derive(Debug, Eq, PartialEq)] +struct TestReceiver; + +impl crate::RealtimeReceiver for TestReceiver { + type ReceiveFuture<'a> + = std::future::Ready> + where + Self: 'a; + + fn receive(&mut self) -> Self::ReceiveFuture<'_> { + return std::future::ready(Ok(crate::TransportReceive::Closed)); + } +} + +struct TestConnection; + +impl crate::RealtimeConnection for TestConnection { + type Sender = TestSender; + type Receiver = TestReceiver; + + fn split(self) -> (Self::Sender, Self::Receiver) { + return (TestSender { closed: false, sent_bytes: 0 }, TestReceiver); + } +} + +fn poll_ready(future: impl core::future::Future) -> T { + let mut future = std::pin::pin!(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + return match core::future::Future::poll(future.as_mut(), &mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => unreachable!("test future unexpectedly remained pending"), + }; +} + +#[test] +fn message_preserves_owned_binary_payload() { + let message = crate::TransportMessage::new(vec![0, 1, 2, 255]); + assert_eq!(message.as_bytes(), &[0, 1, 2, 255]); + assert_eq!(message.len(), 4); + assert!(!message.is_empty()); + assert_eq!(message.into_bytes(), vec![0, 1, 2, 255]); +} + +#[test] +fn empty_message_remains_a_valid_transport_payload() { + let message = crate::TransportMessage::new(Vec::new()); + assert!(message.is_empty()); + assert_eq!(message.len(), 0); +} + +#[test] +fn clean_remote_close_is_distinct_from_transport_error() { + let mut receiver = TestReceiver; + let received = poll_ready(crate::RealtimeReceiver::receive(&mut receiver)); + assert_eq!(received, Ok(crate::TransportReceive::Closed)); +} + +#[test] +fn connection_split_produces_independent_contract_halves() { + let (mut sender, mut receiver) = crate::RealtimeConnection::split(TestConnection); + assert_eq!(poll_ready(crate::RealtimeSender::send(&mut sender, crate::TransportMessage::new(vec![1, 2, 3]))), Ok(())); + assert_eq!(sender.sent_bytes, 3); + assert_eq!(poll_ready(crate::RealtimeReceiver::receive(&mut receiver)), Ok(crate::TransportReceive::Closed)); + assert_eq!(poll_ready(crate::RealtimeSender::close(&mut sender)), Ok(())); + assert!(sender.closed); +} + +#[test] +fn message_receive_variant_preserves_payload() { + let received = crate::TransportReceive::Message(crate::TransportMessage::new(vec![4, 5, 6])); + assert_eq!(received, crate::TransportReceive::Message(crate::TransportMessage::new(vec![4, 5, 6]))); +} + +#[test] +fn transport_error_preserves_stable_kind_and_diagnostic_detail() { + let error = crate::TransportError::new(crate::TransportErrorKind::Timeout, "send deadline exceeded"); + assert_eq!(error.kind(), crate::TransportErrorKind::Timeout); + assert_eq!(error.detail(), "send deadline exceeded"); + assert_eq!(error.to_string(), "operation timed out: send deadline exceeded"); +} + +#[test] +fn transport_error_kinds_have_stable_human_readable_labels() { + let cases = [ + (crate::TransportErrorKind::InvalidConfiguration, "invalid configuration"), + (crate::TransportErrorKind::Connect, "connect failure"), + (crate::TransportErrorKind::Bind, "bind failure"), + (crate::TransportErrorKind::Accept, "accept failure"), + (crate::TransportErrorKind::Timeout, "operation timed out"), + (crate::TransportErrorKind::MessageTooLarge, "message too large"), + (crate::TransportErrorKind::Backpressure, "transport backpressure"), + (crate::TransportErrorKind::Closed, "connection closed"), + (crate::TransportErrorKind::Io, "I/O failure"), + (crate::TransportErrorKind::Protocol, "protocol failure"), + (crate::TransportErrorKind::Aborted, "operation aborted"), + ]; + for (kind, expected) in cases { + assert_eq!(kind.to_string(), expected); + } +} diff --git a/deltas/0.3.4/alpha.2.md b/deltas/0.3.4/alpha.2.md new file mode 100644 index 0000000..aed4744 --- /dev/null +++ b/deltas/0.3.4/alpha.2.md @@ -0,0 +1,133 @@ + + + +# Delta 0.3.4-alpha.2 + +## Base + +Base : `0.3.4-alpha.1` validée par l'utilisateur le 2026-09-21. + +Cette tranche matérialise uniquement la frontière transport-neutral prévue par le plan `0.3.4`. Elle n'ajoute encore ni Tokio, ni Tungstenite, ni socket, ni protocole de session/gameplay. + +## Historique fermé + +Ajout de : + +```text +history/0.3.4/alpha.1.md +``` + +L'entrée enregistre les gates effectivement fournies par l'utilisateur pour `alpha.1`, notamment le rebuild complet après `cargo clean`, les audits propres, `cargo check --workspace` et Clippy workspace strict. + +## Nouvelle crate `game-realtime-transport-lib` + +Ajout de : + +```text +crates/common/game-realtime-transport-lib/Cargo.toml +crates/common/game-realtime-transport-lib/src/lib.rs +crates/common/game-realtime-transport-lib/src/connection.rs +crates/common/game-realtime-transport-lib/src/error.rs +crates/common/game-realtime-transport-lib/src/message.rs +crates/common/game-realtime-transport-lib/unit_tests/contract.rs +``` + +La crate appartient à `crates/common/` car le contrat realtime est indépendant d'une génération de moteur et d'un backend réseau concret. + +Elle ne possède aucune dépendance tierce. + +## Contrat public + +Le payload transport est `TransportMessage`, un buffer binaire possédé sans sémantique gameplay ni codec. + +`TransportReceive` distingue explicitement : + +```text +Message(TransportMessage) +Closed +``` + +Une fermeture distante propre n'est donc pas confondue avec une erreur I/O. + +Les contrats sont : + +```text +RealtimeConnection +RealtimeSender +RealtimeReceiver +``` + +`RealtimeConnection::split()` produit les deux moitiés indépendantes. Les opérations `send`, `close` et `receive` restent async sans imposer un runtime particulier. + +L'implémentation du contrat utilise des futures associées GAT. Cette forme évite une allocation `Box`, conserve le dispatch statique et n'impose pas de borne `Send` au niveau transport-neutral. Un backend natif reste libre de fournir des futures `Send`; un backend navigateur/WASM futur n'est pas exclu artificiellement. + +## Erreurs + +`TransportError` conserve une catégorie stable `TransportErrorKind` et un détail diagnostic backend-neutral. + +Les catégories de baseline sont : + +```text +InvalidConfiguration +Connect +Bind +Accept +Timeout +MessageTooLarge +Backpressure +Closed +Io +Protocol +Aborted +``` + +Les futurs types d'erreur Tungstenite ne sont pas exposés par cette crate. + +## Tests + +Les tests unitaires couvrent : + +- conservation du payload binaire possédé, y compris le payload vide ; +- distinction message/fermeture propre ; +- split sender/receiver ; +- futures associées de send/receive/close avec une implémentation test sans runtime ; +- catégorie, détail et libellés des erreurs. + +Aucun test réseau n'existe dans cette tranche puisqu'aucun backend réseau n'existe encore. + +## Documentation locale + +Aucun `README.md` ni `USAGE.md` local n'est ajouté pendant `alpha.2`. La crate est volontairement petite, son contrat est documenté par rustdoc et le plan central couvre encore ses frontières ; créer un guide local maintenant dupliquerait ces sources sans valeur durable supplémentaire. Ce choix sera réévalué pendant la consolidation finale conformément à `DOC-CRATE-001`. + +## Fichiers existants modifiés + +```text +Cargo.toml +README.md +docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md +``` + +`Cargo.toml` ajoute le membre workspace et passe la version technique à `0.3.4-alpha.2`. + +Le README racine annonce la candidate active. Le plan enregistre la validation de `alpha.1` et ferme le choix des futures associées GAT pour le contrat commun. + +`ROADMAP.md` et `CHANGELOG.md` ne changent pas : le scope macroscopique de `0.3.4` est inchangé et une alpha n'ajoute normalement pas d'entrée de changelog. + +## Validation attendue + +Appliquer d'abord le formatage canonique, puis exécuter : + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history +python3 scripts/audit_distribution_layout.py + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p game-realtime-transport-lib --all-targets --all-features +``` + +Aucune gate Android, Web/Tauri ou réseau n'est requise : cette tranche n'affecte ni leurs contrats ni un backend socket. diff --git a/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md b/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md index 6a3f041..8d1cf6a 100644 --- a/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md +++ b/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md @@ -1,5 +1,5 @@ - + # Plan 0.3.4 — transport realtime et baseline WebSocket @@ -7,7 +7,7 @@ Plan actif créé pendant `0.3.4-alpha.1` à partir de l'archive taggée `v0.3.3`. -La tranche `alpha.1` migre d'abord la gouvernance de version vers `alpha.N / beta.N / rc.N`, audite la baseline et ferme les décisions d'ownership avant toute implémentation réseau lourde. +La tranche `alpha.1` a été validée le 2026-09-21. La tranche active `alpha.2` matérialise maintenant le contrat transport-neutral sans dépendance runtime/backend ; le backend WebSocket reste réservé à `alpha.3`. ## Mission @@ -166,7 +166,9 @@ sender.close() -> async Result receiver.receive() -> async Result ``` -L'implémentation Rust exacte est décidée dans `alpha.2`, avec priorité à une API statiquement dispatchée et sans allocation de futures imposée uniquement pour obtenir un trait object. +`alpha.2` retient une API statiquement dispatchée fondée sur des associated types : `RealtimeConnection` produit un `Sender` et un `Receiver`, puis les opérations async exposent des futures associées GAT (`SendFuture`, `CloseFuture`, `ReceiveFuture`). Le contrat commun n'utilise ni `async-trait`, ni `Box`, ni runtime concret. + +Aucune borne `Send` n'est imposée aux futures par le contrat transport-neutral : un backend natif peut naturellement fournir des futures `Send`, tandis qu'un futur backend navigateur/WASM ne doit pas être rendu impossible par une contrainte de threading qui ne relève pas du transport abstrait. Le contrat doit supporter lecture et écriture concurrentes après split. Il n'expose pas de runtime Tokio et ne crée aucun executor. @@ -391,6 +393,7 @@ Aucune dépendance réseau n'est ajoutée. - créer `game-realtime-transport-lib` ; - implémenter message, receive/close, erreurs et split ; - conserver l'API sans Tokio/Tungstenite ; +- retenir des futures associées GAT afin de préserver le dispatch statique sans box ni contrainte `Send` imposée au contrat ; - tests unitaires ciblés ; - README/USAGE uniquement si une valeur durable est démontrée par `DOC-CRATE-*`. diff --git a/history/0.3.4/alpha.1.md b/history/0.3.4/alpha.1.md new file mode 100644 index 0000000..518e483 --- /dev/null +++ b/history/0.3.4/alpha.1.md @@ -0,0 +1,39 @@ + + + +# Historique 0.3.4-alpha.1 + +## Statut + +`0.3.4-alpha.1` a été validée par l'utilisateur le 2026-09-21 après application du delta sur son checkout issu de la stable `0.3.3`. + +Aucun `alpha.1.fix.N` n'est requis. La suite peut ouvrir `0.3.4-alpha.2` et matérialiser l'API transport-neutral planifiée. + +## Nettoyage et gates statiques + +L'utilisateur a d'abord exécuté : + +```text +cargo clean +Removed 57247 files, 28.5GiB total +``` + +Puis les audits et gates Rust ont tous terminé avec succès : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +games.sasedev workspace audit: clean +Markdown table audit: clean (5 table(s), 260 file(s)) +Distribution layout audit: clean (49 required path(s), 8 forbidden path(s) absent) +cargo fmt --all: clean +cargo fmt --all -- --check: clean +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +``` + +La reconstruction complète après `cargo clean` confirme notamment le workspace à `0.3.4-alpha.1` sur les moteurs, jeux, adapters Desktop/Android/WASM/Tauri et bibliothèques communes. + +## Conséquence + +La migration de gouvernance et le cadrage de `0.3.4` sont acceptés. `alpha.2` peut ajouter uniquement le contrat commun realtime, sans Tokio, Tungstenite ni backend WebSocket ; ces dépendances restent réservées à la tranche suivante.