0.1.0-0-pre.4

This commit is contained in:
2026-09-16 01:01:09 +02:00
parent 2c79a05dd6
commit dc22011997
38 changed files with 540 additions and 131 deletions

View File

@@ -1,8 +1,18 @@
<!-- file: CHANGELOG.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Changelog
## 0.1.0-0-pre.4 — 2026-09-16
- validation de `0-pre.3` enregistrée avec suite workspace complète et smokes Desktop propres ;
- déplacement des tests unitaires hors `src/` vers `unit_tests/` et formalisation de `tests/` pour intégration/environnement ;
- ajout dun audit empêchant le retour de corps de tests sous `src/` ;
- adoption des tests Cargo ciblés par défaut, la suite workspace complète devenant une gate lourde périodique ;
- `cargo fmt --all` devient obligatoire avant la gate lorsquun delta modifie du Rust, sans incrément den-tête pour les seules modifications rustfmt ;
- introduction de `game-logging-lib` avec `tracing`, `tracing-subscriber` et `tracing-appender` ;
- initialisation du tracing dans les runners Desktop et helper de tracing local pour les tests.
## 0.1.0-0-pre.3 — 2026-09-16
- validation utilisateur de `0.1.0-0-pre.2` avec formatage, audits, check, Clippy strict et tests workspace propres ;

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 4
# version: 5
[workspace]
resolver = "3"
@@ -11,10 +11,11 @@ members = [
"crates/games/game-snake-poc",
"crates/apps/game-reflex-poc-desktop",
"crates/apps/game-snake-poc-desktop",
"crates/common/game-logging-lib",
]
[workspace.package]
version = "0.1.0-0-pre.3"
version = "0.1.0-0-pre.4"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games"
@@ -22,6 +23,9 @@ authors = ["Sasedev <games@sasedev.com>"]
publish = false
[workspace.dependencies]
tracing = "0.1.44"
tracing-appender = "0.2.5"
tracing-subscriber = { version = "0.3.23", features = ["fmt"] }
[workspace.lints.rust]
missing_docs = "warn"

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# games.sasedev
@@ -36,3 +36,7 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna
- [`docs/architecture/001-WORKSPACE_ARCHITECTURE.md`](docs/architecture/001-WORKSPACE_ARCHITECTURE.md)
- [`docs/objectives/001-PROJECT_OBJECTIVES.md`](docs/objectives/001-PROJECT_OBJECTIVES.md)
- [`docs/games/001-GAME_CLASSIFICATION.md`](docs/games/001-GAME_CLASSIFICATION.md)
## Diagnostics et tests
Le socle transverse `crates/common/game-logging-lib` fournit le tracing commun. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests dintégration/environnement résident sous `tests/`.

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Roadmap
@@ -8,12 +8,13 @@
- [x] `0-pre.1` — squelette du workspace, règles, architecture, versions, assets, Java Android commun/spécifique et deux crates POC.
- [x] `0-pre.2` — consolidation documentaire issue de l'architecture de référence, runners Desktop par jeu et politique normative des commandes.
- [x] `0-pre.3` — première boucle moteur exécutable Desktop : temps, input abstrait, capacités de monétisation optionnelles et état de jeu minimal via les runners.
- [ ] `0-pre.4`intégration SDL3 Desktop réelle et premier rendu POC Reflex.
- [ ] `0-pre.5`projet Gradle Android exécutable, SDL3 AAR/NDK, compilation Rust `cdylib`, lancement sur appareil/émulateur.
- [ ] `0-pre.6`bridge Java/JNI minimal et input tactile Android.
- [ ] `0-pre.7`assets communs + spécifiques empaquetés sans copie dans les crates.
- [ ] `0-pre.8`POC Reflex jouable Desktop + Android.
- [ ] `0-pre.9` — POC Snake jouable et validation de la réutilisation du moteur.
- [x] `0-pre.4`architecture de tests hors `src/`, stratégie de tests ciblés et socle `tracing`/`tracing-subscriber`/`tracing-appender`.
- [ ] `0-pre.5`intégration SDL3 Desktop réelle et premier rendu POC Reflex.
- [ ] `0-pre.6`projet Gradle Android exécutable, SDL3 AAR/NDK, compilation Rust `cdylib`, lancement sur appareil/émulateur.
- [ ] `0-pre.7`bridge Java/JNI minimal et input tactile Android.
- [ ] `0-pre.8`assets communs + spécifiques empaquetés sans copie dans les crates.
- [ ] `0-pre.9` — POC Reflex jouable Desktop + Android.
- [ ] `0-pre.10` — POC Snake jouable et validation de la réutilisation du moteur.
- [ ] `1-alpha.1` — première API moteur V1 volontairement stabilisée.
- [ ] `2-beta.1` — stabilisation, packaging, tests multi-appareils.
- [ ] `3-rc.1` — candidat de release du socle 0.1.0.

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-reflex-poc-desktop/Cargo.toml
# version: 2
# version: 3
[package]
name = "game-reflex-poc-desktop"
@@ -11,6 +11,8 @@ authors.workspace = true
publish.workspace = true
[dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
tracing.workspace = true
engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-reflex-poc = { path = "../../games/game-reflex-poc" }

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-desktop/src/main.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -10,6 +10,14 @@
//! This binary exercises the platform-independent fixed-step loop. SDL3 windowing and physical input mapping are introduced in the next dedicated delta.
fn main() {
let _logging_guard = match game_logging_lib::init_console_tracing() {
std::result::Result::Ok(guard) => guard,
std::result::Result::Err(error) => {
eprintln!("failed to initialize tracing: {error}");
return;
},
};
tracing::info!(target: "games::runner", game = "reflex", "desktop POC runner started");
let _monetization = engine_v1_platform_api::MonetizationCapabilities::disabled();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
let mut runner = engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(16));
@@ -17,6 +25,7 @@ fn main() {
for _ in 0..3 {
runner.tick(&mut state, input);
}
tracing::info!(target: "games::runner", game = "reflex", frames = runner.next_frame_index(), "desktop POC runner completed");
println!("Reflex POC desktop runner: score={} frames={}", state.score(), runner.next_frame_index());
return;
}

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-snake-poc-desktop/Cargo.toml
# version: 2
# version: 3
[package]
name = "game-snake-poc-desktop"
@@ -11,6 +11,8 @@ authors.workspace = true
publish.workspace = true
[dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
tracing.workspace = true
engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-snake-poc = { path = "../../games/game-snake-poc" }

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-snake-poc-desktop/src/main.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -10,6 +10,14 @@
//! This binary exercises the platform-independent fixed-step loop. SDL3 windowing and physical input mapping are introduced in the next dedicated delta.
fn main() {
let _logging_guard = match game_logging_lib::init_console_tracing() {
std::result::Result::Ok(guard) => guard,
std::result::Result::Err(error) => {
eprintln!("failed to initialize tracing: {error}");
return;
},
};
tracing::info!(target: "games::runner", game = "snake", "desktop POC runner started");
let _monetization = engine_v1_platform_api::MonetizationCapabilities::disabled();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
let mut runner = engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(16));
@@ -17,6 +25,7 @@ fn main() {
for _ in 0..3 {
runner.tick(&mut state, input);
}
tracing::info!(target: "games::runner", game = "snake", frames = runner.next_frame_index(), "desktop POC runner completed");
println!("Snake POC desktop runner: length={} frames={}", state.length(), runner.next_frame_index());
return;
}

View File

@@ -0,0 +1,19 @@
# file: crates/common/game-logging-lib/Cargo.toml
# version: 1
[package]
name = "game-logging-lib"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[dependencies]
tracing.workspace = true
tracing-appender.workspace = true
tracing-subscriber.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,20 @@
// file: crates/common/game-logging-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Shared tracing initialization and test diagnostics for games.sasedev applications and crates.
mod runtime;
mod test_support;
/// Re-export of the console tracing initialization error.
pub use self::runtime::LoggingInitError;
/// Re-export of the guard keeping the non-blocking tracing writer alive.
pub use self::runtime::LoggingWorkerGuard;
/// Re-export of the standard console tracing initializer.
pub use self::runtime::init_console_tracing;
/// Re-export of the scoped tracing helper intended for tests.
pub use self::test_support::with_test_tracing;

View File

@@ -0,0 +1,31 @@
// file: crates/common/game-logging-lib/src/runtime.rs
// version: 1
/// Error returned when the process-global tracing subscriber is already configured.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LoggingInitError;
impl std::fmt::Display for LoggingInitError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("the process-global tracing subscriber is already configured");
}
}
impl std::error::Error for LoggingInitError {}
/// Guard keeping the non-blocking tracing writer alive for the application lifetime.
pub struct LoggingWorkerGuard {
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
}
/// Initializes a process-global non-blocking tracing subscriber writing formatted events to standard error.
///
/// The returned guard must remain alive for as long as events may still be emitted.
pub fn init_console_tracing() -> std::result::Result<LoggingWorkerGuard, LoggingInitError> {
let (writer, worker_guard) = tracing_appender::non_blocking(std::io::stderr());
let subscriber = tracing_subscriber::fmt().with_target(true).with_writer(writer).finish();
if tracing::subscriber::set_global_default(subscriber).is_err() {
return std::result::Result::Err(LoggingInitError);
}
return std::result::Result::Ok(LoggingWorkerGuard { _worker_guard: worker_guard });
}

View File

@@ -0,0 +1,13 @@
// file: crates/common/game-logging-lib/src/test_support.rs
// version: 1
/// Executes one test body with a thread-local tracing subscriber configured for the Rust test writer.
pub fn with_test_tracing<T>(test_name: &str, operation: impl FnOnce() -> T) -> T {
let subscriber = tracing_subscriber::fmt().with_target(true).with_test_writer().without_time().finish();
return tracing::subscriber::with_default(subscriber, || {
tracing::debug!(test_name = test_name, "test started");
let result = operation();
tracing::debug!(test_name = test_name, "test completed");
return result;
});
}

View File

@@ -0,0 +1,11 @@
// file: crates/common/game-logging-lib/tests/test_support.rs
// version: 1
#[test]
fn scoped_test_tracing_returns_test_result() {
let result = game_logging_lib::with_test_tracing("scoped_test_tracing_returns_test_result", || {
tracing::info!(value = 41_u32, "integration test event");
return 42_u32;
});
assert_eq!(result, 42_u32);
}

View File

@@ -1,5 +1,5 @@
# file: crates/engines/engine-v1-common/Cargo.toml
# version: 1
# version: 2
[package]
name = "engine-v1-common"
@@ -10,5 +10,8 @@ repository.workspace = true
authors.workspace = true
publish.workspace = true
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-common/src/game_loop.rs
// version: 1
// version: 2
/// Minimal update contract implemented by a game state consumed by engine V1.
pub trait EngineGame {
@@ -54,30 +54,5 @@ impl FixedStepRunner {
}
#[cfg(test)]
mod unit_tests {
#[derive(Default)]
struct CountingGame {
updates: u64,
last_frame: Option<crate::EngineFrame>,
}
impl crate::EngineGame for CountingGame {
fn update(&mut self, frame: crate::EngineFrame, _input: crate::InputState) {
self.updates = self.updates.saturating_add(1);
self.last_frame = Some(frame);
return;
}
}
#[test]
fn fixed_step_runner_advances_index_and_elapsed_time() {
let mut game = CountingGame::default();
let mut runner = crate::FixedStepRunner::new(std::time::Duration::from_millis(16));
runner.tick(&mut game, crate::InputState::none());
runner.tick(&mut game, crate::InputState::none());
assert_eq!(game.updates, 2);
assert_eq!(runner.next_frame_index(), 2);
assert_eq!(runner.elapsed(), std::time::Duration::from_millis(32));
assert_eq!(game.last_frame.map(crate::EngineFrame::index), Some(1));
}
}
#[path = "../unit_tests/game_loop.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-common/src/input.rs
// version: 1
// version: 2
/// Platform-independent snapshot of logical input actions for one update.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -51,11 +51,5 @@ impl InputState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn action_state_is_independent() {
let state = crate::InputState::none().with_action(crate::GameAction::Primary, true);
assert!(state.is_active(crate::GameAction::Primary));
assert!(!state.is_active(crate::GameAction::Secondary));
}
}
#[path = "../unit_tests/input.rs"]
mod tests;

View File

@@ -0,0 +1,30 @@
// file: crates/engines/engine-v1-common/unit_tests/game_loop.rs
// version: 1
#[derive(Default)]
struct CountingGame {
updates: u64,
last_frame: std::option::Option<crate::EngineFrame>,
}
impl crate::EngineGame for CountingGame {
fn update(&mut self, frame: crate::EngineFrame, _input: crate::InputState) {
self.updates = self.updates.saturating_add(1);
self.last_frame = std::option::Option::Some(frame);
return;
}
}
#[test]
fn fixed_step_runner_advances_index_and_elapsed_time() {
game_logging_lib::with_test_tracing("fixed_step_runner_advances_index_and_elapsed_time", || {
let mut game = CountingGame::default();
let mut runner = crate::FixedStepRunner::new(std::time::Duration::from_millis(16));
runner.tick(&mut game, crate::InputState::none());
runner.tick(&mut game, crate::InputState::none());
assert_eq!(game.updates, 2);
assert_eq!(runner.next_frame_index(), 2);
assert_eq!(runner.elapsed(), std::time::Duration::from_millis(32));
assert_eq!(game.last_frame.map(crate::EngineFrame::index), std::option::Option::Some(1));
});
}

View File

@@ -0,0 +1,12 @@
// file: crates/engines/engine-v1-common/unit_tests/input.rs
// version: 1
#[test]
fn action_state_is_independent() {
game_logging_lib::with_test_tracing("action_state_is_independent", || {
let input = crate::InputState::none().with_action(crate::GameAction::Primary, true);
assert!(input.is_active(crate::GameAction::Primary));
assert!(!input.is_active(crate::GameAction::Secondary));
assert!(!input.is_active(crate::GameAction::Left));
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/engines/engine-v1-platform-api/Cargo.toml
# version: 1
# version: 2
[package]
name = "engine-v1-platform-api"
@@ -10,5 +10,8 @@ repository.workspace = true
authors.workspace = true
publish.workspace = true
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-platform-api/src/service.rs
// version: 2
// version: 3
/// Platform capabilities that may have different implementations per target.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -61,11 +61,5 @@ impl MonetizationCapabilities {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn disabled_monetization_disables_both_services() {
let capabilities = crate::MonetizationCapabilities::disabled();
assert_eq!(capabilities.advertising(), crate::PlatformServiceAvailability::Disabled);
assert_eq!(capabilities.billing(), crate::PlatformServiceAvailability::Disabled);
}
}
#[path = "../unit_tests/service.rs"]
mod tests;

View File

@@ -0,0 +1,11 @@
// file: crates/engines/engine-v1-platform-api/unit_tests/service.rs
// version: 1
#[test]
fn disabled_monetization_disables_both_services() {
game_logging_lib::with_test_tracing("disabled_monetization_disables_both_services", || {
let capabilities = crate::MonetizationCapabilities::disabled();
assert_eq!(capabilities.advertising(), crate::PlatformServiceAvailability::Disabled);
assert_eq!(capabilities.billing(), crate::PlatformServiceAvailability::Disabled);
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/games/game-reflex-poc/Cargo.toml
# version: 1
# version: 2
[package]
name = "game-reflex-poc"
@@ -15,5 +15,8 @@ engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
engine-v1-sdl = { path = "../../engines/engine-v1-sdl" }
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/games/game-reflex-poc/src/state.rs
// version: 2
// version: 3
/// Minimal deterministic state used to validate the first game boundary.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -36,23 +36,5 @@ impl engine_v1_common::EngineGame for ReflexState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn success_increments_score() {
let mut state = crate::ReflexState::new();
state.register_success();
assert_eq!(state.score(), 1);
}
#[test]
fn engine_update_maps_primary_action_to_success() {
let mut state = crate::ReflexState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.score(), 1);
}
}
#[path = "../unit_tests/state.rs"]
mod tests;

View File

@@ -0,0 +1,25 @@
// file: crates/games/game-reflex-poc/unit_tests/state.rs
// version: 1
#[test]
fn success_increments_score() {
game_logging_lib::with_test_tracing("success_increments_score", || {
let mut state = crate::ReflexState::new();
state.register_success();
assert_eq!(state.score(), 1);
});
}
#[test]
fn engine_update_maps_primary_action_to_success() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_success", || {
let mut state = crate::ReflexState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.score(), 1);
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/games/game-snake-poc/Cargo.toml
# version: 1
# version: 2
[package]
name = "game-snake-poc"
@@ -15,5 +15,8 @@ engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
engine-v1-sdl = { path = "../../engines/engine-v1-sdl" }
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/games/game-snake-poc/src/state.rs
// version: 2
// version: 3
/// Minimal Snake state used to validate reusable engine dependencies.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -42,23 +42,5 @@ impl engine_v1_common::EngineGame for SnakeState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn growth_increments_length() {
let mut state = crate::SnakeState::new();
state.grow();
assert_eq!(state.length(), 2);
}
#[test]
fn engine_update_maps_primary_action_to_growth() {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.length(), 2);
}
}
#[path = "../unit_tests/state.rs"]
mod tests;

View File

@@ -0,0 +1,25 @@
// file: crates/games/game-snake-poc/unit_tests/state.rs
// version: 1
#[test]
fn growth_increments_length() {
game_logging_lib::with_test_tracing("growth_increments_length", || {
let mut state = crate::SnakeState::new();
state.grow();
assert_eq!(state.length(), 2);
});
}
#[test]
fn engine_update_maps_primary_action_to_growth() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_growth", || {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.length(), 2);
});
}

127
deltas/0.1.0/0-pre.4.md Normal file
View File

@@ -0,0 +1,127 @@
<!-- file: deltas/0.1.0/0-pre.4.md -->
<!-- version: 1 -->
# Delta 0.1.0-0-pre.4
## Base
Base déclarée : `0.1.0-0-pre.3` validée par l'utilisateur le 2026-09-16.
## Validation du delta précédent
L'utilisateur a exécuté :
```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 deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets --all-features
cargo run -p game-reflex-poc-desktop
cargo run -p game-snake-poc-desktop
```
Résultat communiqué : toutes les gates sont propres. Les tests workspace ont validé les deux tests `engine-v1-common`, le test `engine-v1-platform-api`, les deux tests Reflex et les deux tests Snake. Les smokes ont produit :
```text
Reflex POC desktop runner: score=3 frames=3
Snake POC desktop runner: length=4 frames=3
```
`0.1.0-0-pre.3` est donc accepté comme base de ce delta.
## Objectifs
- sortir tous les corps de tests unitaires de `src/` selon le modèle KSP ;
- réserver `unit_tests/` aux tests unitaires rattachés aux modules et `tests/` aux tests d'intégration/environnement/smoke ;
- rendre cette séparation mécaniquement vérifiable ;
- rendre `cargo fmt --all` obligatoire lorsqu'un delta a modifié du Rust, avant la gate `--check` ;
- remplacer les tests workspace systématiques par une sélection ciblée par défaut ;
- introduire le socle de diagnostics structurés fondé sur `tracing`, `tracing-subscriber` et `tracing-appender` ;
- utiliser ce socle dans les runners et dans les tests sans initialisations globales concurrentes.
## Tests hors `src/`
Les tests unitaires existants sont déplacés vers les répertoires `unit_tests/` des crates concernées. Les modules de production utilisent désormais le pattern :
```rust
#[cfg(test)]
#[path = "../unit_tests/module.rs"]
mod tests;
```
Le nouvel audit projet refuse un corps `#[test]` sous `src/` et exige qu'un `#[cfg(test)]` de production référence un fichier externe sous `unit_tests/`.
## Tracing
La nouvelle crate `crates/common/game-logging-lib` fournit :
- `init_console_tracing()` pour les applications ;
- un writer non bloquant via `tracing-appender` ;
- `LoggingWorkerGuard` pour conserver le worker vivant ;
- `with_test_tracing()` pour installer un subscriber local au test sans conflit de subscriber global.
Les deux runners Desktop initialisent désormais le tracing et les tests unitaires utilisent le helper de test commun.
## Politique des tests
La suite suivante n'est plus requise à chaque delta :
```bash
cargo test --workspace --all-targets --all-features
```
Elle reste requise au démarrage d'une nouvelle version `X.Y.Z`, à la fin d'une phase/version, après un changement transversal important ou lorsqu'il existe un doute sur la portée des tests ciblés.
## Validation exécutée pendant la préparation
Les audits Python et contrôles de structure disponibles dans l'environnement de génération sont exécutés. Les commandes Cargo finales restent exécutées par l'utilisateur.
## Commandes de validation à exécuter par l'utilisateur
Le delta modifie du Rust ; commencer obligatoirement par le formatage mutatif puis sa vérification :
```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 deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
```
Tests ciblés requis pour ce delta :
```bash
cargo test -p game-logging-lib --all-targets --all-features
cargo test -p engine-v1-common --all-targets --all-features
cargo test -p engine-v1-platform-api --all-targets --all-features
cargo test -p game-reflex-poc --all-targets --all-features
cargo test -p game-snake-poc --all-targets --all-features
```
La suite workspace complète n'est pas demandée pour `0-pre.4`, car elle vient d'être exécutée proprement sur `0-pre.3` et l'impact de ce delta est borné par les tests ciblés ci-dessus.
Les runners ont changé pour initialiser le tracing ; rejouer également :
```bash
cargo run -p game-reflex-poc-desktop
cargo run -p game-snake-poc-desktop
```
Les lignes fonctionnelles finales attendues restent :
```text
Reflex POC desktop runner: score=3 frames=3
Snake POC desktop runner: length=4 frames=3
```
Des événements `tracing` supplémentaires peuvent apparaître autour de ces lignes.
## Règle de transition
Si les validations demandées sont propres, `0.1.0-0-pre.4` est accepté et le travail passe automatiquement à `0.1.0-0-pre.5`, consacré à l'intégration SDL3 Desktop réelle.
Si une validation échoue, ne pas passer à `0-pre.5` : produire `0.1.0-0-pre.4.fix.1`, puis incrémenter le suffixe de fix si nécessaire.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Documentation games.sasedev
@@ -44,3 +44,8 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R
- [`validation/001-VALIDATION_GATES.md`](validation/001-VALIDATION_GATES.md) — gates manuelles du workspace.
- [`development/002-DESKTOP_DISTRIBUTIONS.md`](development/002-DESKTOP_DISTRIBUTIONS.md) — distributions Desktop natives et variante Tauri optionnelle.
## Tests et diagnostics
- [`testing/001-TEST_ARCHITECTURE.md`](testing/001-TEST_ARCHITECTURE.md) — séparation `unit_tests/` / `tests/`, tests ciblés et tracing de test.
- [`development/003-TRACING_AND_DIAGNOSTICS.md`](development/003-TRACING_AND_DIAGNOSTICS.md) — socle `tracing`, subscriber et appender communs.

View File

@@ -0,0 +1,23 @@
<!-- file: docs/development/003-TRACING_AND_DIAGNOSTICS.md -->
<!-- version: 1 -->
# Tracing et diagnostics
## Objectif
Le projet utilise `tracing` comme façade structurée commune pour les événements de diagnostic Rust. Les applications choisissent le subscriber adapté à leur distribution ; les librairies émettent des événements sans imposer une destination globale.
## Socle initial
`crates/common/game-logging-lib` regroupe linitialisation réutilisable. La première implémentation fournit :
- un subscriber console formaté ;
- une écriture non bloquante via `tracing-appender` ;
- un guard dont la durée de vie protège le flush des événements ;
- un subscriber local destiné aux tests.
Les runners Desktop initialisent ce socle. Les futures intégrations Android pourront adapter la destination aux contraintes Logcat/plateforme sans modifier les crates jeu.
## Évolution
Les filtres par target/niveau, fichiers rotatifs, configuration utilisateur, traces Android et éventuelle télémétrie distante seront ajoutés uniquement lorsquun besoin concret les justifie.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/FILE_CONTRACTS.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Contrats des fichiers principaux
@@ -16,6 +16,8 @@
- `docs/monetization/` décrit les modèles de monétisation indépendamment des fournisseurs.
- `docs/services/` décrit leaderboards, backend, partage, anti-cheat et services communautaires.
- `docs/development/` décrit les workflows de développement non normatifs complémentaires aux règles.
- `docs/testing/` décrit larchitecture des tests, leur placement et les stratégies de sélection.
- `crates/common/` contient les bibliothèques transverses indépendantes dune génération spécifique du moteur, notamment le socle de logging.
- `deltas/` contient un document par livraison ou correctif versionné.
- `prompts/` peut contenir les prompts de reprise de session lorsqu'ils deviennent utiles.
- `scripts/` contient des audits en lecture seule et des outils du dépôt.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_COMMANDS.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Règles d'exécution des commandes
@@ -9,7 +9,7 @@
- **CMD-GEN-002** — Une commande mutante n'est pas utilisée lorsqu'une commande de contrôle en lecture seule suffit.
- **CMD-GEN-003** — Les commandes destructives ou de nettoyage global ne sont jamais exécutées par habitude.
- **CMD-GEN-004** — Une gate n'est déclarée réussie que si la commande exacte a été exécutée sur l'état livré.
- **CMD-GEN-005** — Les commandes ciblées sont préférées pendant le développement ; les commandes workspace complètes sont utilisées aux gates de livraison.
- **CMD-GEN-005** — Les commandes ciblées sont préférées dès que leur portée est connue ; les gates workspace complètes ne sont imposées que lorsque leur coût est justifié par la portée du changement ou la phase de validation.
- **CMD-GEN-006** — Les scripts `audit_*` sont des contrôles en lecture seule et ne corrigent jamais automatiquement les fichiers.
- **CMD-GEN-007** — Sauf demande explicite contraire, les commandes Cargo de validation finale sont exécutées par l'utilisateur sur son workspace local et non déclarées réussies par le générateur.
- **CMD-GEN-008** — Chaque fichier delta indique les commandes de validation que l'utilisateur doit exécuter et le statut connu de la validation du delta précédent.
@@ -18,17 +18,18 @@
## Rust et Cargo
- **CMD-RUST-001** — `cargo fmt --all` peut être utilisé après une tranche cohérente pour appliquer le formatage ; il ne sert pas de diagnostic.
- **CMD-RUST-002** — `cargo fmt --all -- --check` est la gate canonique de formatage et doit être exécutée avant livraison.
- **CMD-RUST-001** — Lorsquun delta a modifié au moins un fichier Rust, lutilisateur exécute `cargo fmt --all` au début de la validation afin dappliquer le formatage canonique; les changements purement produits par rustfmt constituent lunique exception à lincrément obligatoire de len-tête de version du fichier.
- **CMD-RUST-002** — `cargo fmt --all -- --check` suit immédiatement le formatage et constitue la gate canonique de conformité rustfmt.
- **CMD-RUST-003** — `cargo check --workspace` est la première gate de compilation globale après les audits statiques.
- **CMD-RUST-004** — `cargo clippy --workspace --all-targets --all-features -- -D warnings` est exécuté après un `cargo check --workspace` propre pour la gate complète.
- **CMD-RUST-005** — `cargo test --workspace --all-targets --all-features` est la gate de tests globale ; des tests `-p <crate>` peuvent être utilisés plus tôt pendant le développement.
- **CMD-RUST-006** — `cargo run -p <desktop-runner>` sert aux smokes manuels Desktop et n'est pas substitué aux tests automatisés.
- **CMD-RUST-007** — `cargo build` est utilisé lorsqu'un artefact exécutable ou une bibliothèque est réellement nécessaire ; il n'est pas lancé systématiquement en plus de `cargo check`.
- **CMD-RUST-008** — `cargo tree` et ses variantes sont des commandes de diagnostic de dépendances, pas des gates obligatoires de chaque delta.
- **CMD-RUST-009** — `cargo update` n'est jamais exécuté opportunistement. Toute mise à jour de dépendance doit appartenir à une tranche explicitement consacrée aux dépendances ou être nécessaire à la fonctionnalité en cours.
- **CMD-RUST-010** — `cargo clean` n'est pas une gate et n'est pas utilisé en routine. Il n'est autorisé qu'en cas de diagnostic de build corrompu, de contrainte disque explicite ou de demande ciblée, avec justification.
- **CMD-RUST-011** — La suppression manuelle de `target/` ne remplace pas `cargo clean` et suit les mêmes restrictions.
- **CMD-RUST-005** — Les tests ciblés `cargo test -p <crate> --all-targets --all-features` sont la stratégie normale dun delta et doivent couvrir toutes les crates directement ou transitivement affectées lorsque cela est raisonnablement déterminable.
- **CMD-RUST-006** — `cargo test --workspace --all-targets --all-features` est une gate lourde réservée au démarrage dune nouvelle version `X.Y.Z`, à la fin dune phase/version, aux changements transverses importants ou lorsquil existe un doute raisonnable sur la portée des tests ciblés.
- **CMD-RUST-007** — `cargo run -p <desktop-runner>` sert aux smokes manuels Desktop et n'est pas substitué aux tests automatisés.
- **CMD-RUST-008** — `cargo build` est utilisé lorsqu'un artefact exécutable ou une bibliothèque est réellement nécessaire ; il n'est pas lancé systématiquement en plus de `cargo check`.
- **CMD-RUST-009** — `cargo tree` et ses variantes sont des commandes de diagnostic de dépendances, pas des gates obligatoires de chaque delta.
- **CMD-RUST-010** — `cargo update` n'est jamais exécuté opportunistement. Toute mise à jour de dépendance doit appartenir à une tranche explicitement consacrée aux dépendances ou être nécessaire à la fonctionnalité en cours.
- **CMD-RUST-011** — `cargo clean` n'est pas une gate et n'est pas utilisé en routine. Il n'est autorisé qu'en cas de diagnostic de build corrompu, de contrainte disque explicite ou de demande ciblée, avec justification.
- **CMD-RUST-012** — La suppression manuelle de `target/` ne remplace pas `cargo clean` et suit les mêmes restrictions.
## Runners Desktop

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_PROJECT.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Règles spécifiques games.sasedev
@@ -7,7 +7,7 @@
- **GAME-WS-001** — Un seul workspace Cargo racine contient les crates Rust du dépôt.
- **GAME-WS-002** — Toutes les crates Rust résident sous `crates/`.
- **GAME-WS-003** — Les crates moteur résident sous `crates/engines/`, les crates jeu sous `crates/games/` et les exécutables/launchers réutilisant ces libs sous `crates/apps/`.
- **GAME-WS-003** — Les crates moteur résident sous `crates/engines/`, les crates jeu sous `crates/games/`, les bibliothèques transverses indépendantes dune génération de moteur sous `crates/common/` et les exécutables/launchers réutilisant ces libs sous `crates/apps/`.
- **GAME-WS-004** — Une crate hérite par défaut de `workspace.package.version`.
- **GAME-WS-005** — Une crate arrivée à maturité peut porter sa propre version SemVer lorsqu'une décision documentée rend son cycle autonome nécessaire.
- **GAME-WS-006** — Les dépendances tierces communes sont centralisées sous `[workspace.dependencies]` et consommées avec `workspace = true` lorsqu'elles sont partagées.
@@ -54,3 +54,10 @@
- **GAME-POC-001** — Les premiers POC servent à valider l'architecture et doivent rester volontairement petits.
- **GAME-POC-002** — Un POC ne justifie pas l'introduction prématurée d'un ECS, moteur physique ou backend complet s'il n'en a pas besoin.
## Diagnostics structurés
- **GAME-TRACE-001** — `tracing` est le mécanisme Rust canonique pour les diagnostics structurés.
- **GAME-TRACE-002** — `tracing-subscriber` compose les subscribers applicatifs et de test ; une librairie métier ne configure pas silencieusement le subscriber global.
- **GAME-TRACE-003** — `tracing-appender` est utilisé lorsque lécriture non bloquante ou les fichiers de logs deviennent nécessaires ; le guard associé reste vivant pendant toute la durée utile.
- **GAME-TRACE-004** — La configuration de logging commune réside dans une crate transverse et ne doit pas être dupliquée par jeu.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_RUST.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Règles Rust générales
@@ -35,8 +35,9 @@
## Formatage
- **RUST-FMT-001** — `rustfmt.toml` racine est canonique.
- **RUST-FMT-002** — `cargo fmt --all -- --check` fait partie des gates.
- **RUST-FMT-003** — La largeur maximale canonique est 160 caractères.
- **RUST-FMT-002** — Lorsquun delta modifie du code Rust, `cargo fmt --all` est exécuté avant la gate afin dappliquer `rustfmt.toml`; les seules modifications produites par cette commande ne nécessitent pas dincrémenter len-tête `// version: N` des fichiers concernés.
- **RUST-FMT-003** — `cargo fmt --all -- --check` vérifie ensuite que létat livré est conforme au formatage canonique.
- **RUST-FMT-004** — La largeur maximale canonique est 160 caractères.
## Tests

View File

@@ -0,0 +1,28 @@
<!-- file: docs/testing/001-TEST_ARCHITECTURE.md -->
<!-- version: 1 -->
# Architecture des tests
## Séparation physique
Le code de production sous `src/` ne contient aucun corps de test. Les tests unitaires résident dans `unit_tests/` à la racine de la crate et sont rattachés explicitement au module testé :
```rust
#[cfg(test)]
#[path = "../unit_tests/example.rs"]
mod tests;
```
Les tests dintégration, denvironnement et les smokes automatisés résident sous `tests/`, selon le mécanisme Cargo standard.
Cette séparation reprend le modèle utilisé dans KSP : elle garde `src/` exclusivement consacré au code de production tout en permettant aux unit tests daccéder au contexte privé du module auquel ils sont rattachés.
## Tests ciblés
Un delta ne relance pas automatiquement tous les tests du workspace. Les crates affectées sont testées explicitement avec `cargo test -p ... --all-targets --all-features`.
La suite globale est conservée comme gate lourde pour les frontières de version, la stabilisation et les changements transverses dont limpact ne peut pas être borné avec confiance.
## Tracing dans les tests
`game-logging-lib::with_test_tracing` installe un subscriber local au thread pour la durée du corps de test. Il évite les conflits liés à plusieurs initialisations globales et permet aux tests démettre des événements `tracing` consultables avec les mécanismes habituels du test runner.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_GATES.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Gates de validation
@@ -9,19 +9,34 @@ Les audits Python peuvent être exécutés pendant la préparation d'un delta. S
Le générateur ne marque jamais une commande Cargo comme propre s'il ne dispose que du résultat d'un delta antérieur.
## Gate canonique Rust et documentation
## Gate normale dun delta Rust
À exécuter par l'utilisateur pour valider chaque delta contenant du code, du build, des manifests ou des règles affectant le workspace :
Lorsquun delta modifie du code Rust, la validation commence par un formatage mutatif volontaire :
```bash
cargo fmt --all
cargo fmt --all -- --check
```
Les modifications exclusivement produites par `cargo fmt --all` nimposent pas dincrémenter `// version: N`, car elles font partie de la préparation du même état avant commit.
Les audits et gates de compilation restent ensuite :
```bash
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 deltas
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets --all-features
```
Les tests sont ciblés par défaut :
```bash
cargo test -p <crate-affectée> --all-targets --all-features
```
Le fichier delta énumère les crates à tester. `cargo test --workspace --all-targets --all-features` nest demandé systématiquement quau démarrage dune nouvelle version `X.Y.Z`, vers la fin dune phase/version, après un changement transversal important ou lorsque limpact réel ne permet pas de sélectionner avec confiance les tests ciblés.
## Smokes Desktop
Lorsqu'un delta modifie la boucle exécutable, le runtime ou le comportement visible des runners Desktop, exécuter également :

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/000-V0_1_0_START_PROMPT.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Prompt de reprise 0.1.0
@@ -8,3 +8,7 @@ Partir de la dernière livraison `0.1.0-*`, lire `RULES.md`, `ROADMAP.md`, `CHAN
## Validation et enchaînement
La validation Cargo finale d'un delta est exécutée par l'utilisateur sauf demande explicite contraire. Le delta suivant doit reprendre le statut de cette validation. Une gate entièrement propre autorise le passage automatique au delta suivant ; une gate en échec impose un `.fix.N` du delta courant avant progression, sauf décision explicite contraire.
## Formatage, tests et diagnostics
Lorsquun delta modifie du Rust, demander `cargo fmt --all` avant `cargo fmt --all -- --check`. Les seules modifications produites par rustfmt nimposent pas dincrémenter les en-têtes `// version: N`. Préférer les tests `cargo test -p ... --all-targets --all-features` ciblés ; réserver la suite workspace complète aux frontières de version/phase, aux changements transverses ou aux cas où la portée nest pas sûre. Les tests unitaires résident sous `unit_tests/`, les tests dintégration/environnement sous `tests/`, jamais sous `src/`. Utiliser le socle `game-logging-lib`/`tracing` pour les diagnostics structurés lorsque pertinent.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# file: scripts/audit_project_workspace_rules.py
# version: 2
# version: 3
"""Audit mechanically verifiable games.sasedev workspace boundaries."""
@@ -44,6 +44,25 @@ def main() -> int:
errors.append(f"GAME-WS-005: {relative}: explicit crate version does not follow the canonical games.sasedev SemVer scheme")
else:
errors.append(f"GAME-WS-004: {relative}: crate must inherit or explicitly own a version")
for source_path in sorted((root / "crates").rglob("src/**/*.rs")):
relative = source_path.relative_to(root).as_posix()
text = source_path.read_text(encoding="utf-8")
if "#[test]" in text:
errors.append(f"RUST-TEST-001: test body forbidden under src/: {relative}")
if "#[cfg(test)]" in text:
external_test_module = re.search(
r'#\[cfg\(test\)\]\s*#\[path\s*=\s*"[^"]*unit_tests/[^"]+"\]\s*mod\s+tests\s*;',
text,
re.MULTILINE,
)
if external_test_module is None:
errors.append(f"RUST-TEST-001: cfg(test) under src/ must reference an external unit_tests file: {relative}")
for test_dir in sorted((root / "crates").rglob("src/**/unit_tests")):
if test_dir.is_dir():
errors.append(f"RUST-TEST-002: unit_tests directory forbidden under src/: {test_dir.relative_to(root).as_posix()}")
for test_dir in sorted((root / "crates").rglob("src/**/tests")):
if test_dir.is_dir():
errors.append(f"RUST-TEST-003: integration tests directory forbidden under src/: {test_dir.relative_to(root).as_posix()}")
forbidden_assets = [path for path in (root / "crates").rglob("assets") if path.is_dir()]
for path in forbidden_assets:
errors.append(f"GAME-ASSET-001: assets directory forbidden inside crates: {path.relative_to(root).as_posix()}")