0.1.0-1-alpha.2-fix.3

This commit is contained in:
2026-09-17 00:29:56 +02:00
parent 37a00b3c2b
commit b31ffa74f5
17 changed files with 349 additions and 57 deletions

View File

@@ -1,5 +1,5 @@
// file: Android/game-reflex-poc/build.gradle // file: Android/game-reflex-poc/build.gradle
// version: 13 // version: 14
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -18,7 +18,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-1-alpha.2.fix.2' versionName '0.1.0-1-alpha.2.fix.3'
} }
compileOptions { compileOptions {

View File

@@ -1,5 +1,5 @@
// file: Android/game-snake-poc/build.gradle // file: Android/game-snake-poc/build.gradle
// version: 13 // version: 14
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -18,7 +18,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-1-alpha.2.fix.2' versionName '0.1.0-1-alpha.2.fix.3'
} }
compileOptions { compileOptions {

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 30 # version: 31
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -19,7 +19,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.1.0-1-alpha.2.fix.2" version = "0.1.0-1-alpha.2.fix.3"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games" repository = "https://git.sasedev.com/Sasedev/games"

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-reflex-poc-tauri/Cargo.toml # file: crates/apps/game-reflex-poc-tauri/Cargo.toml
# version: 3 # version: 4
[package] [package]
name = "game-reflex-poc-tauri" name = "game-reflex-poc-tauri"
@@ -23,6 +23,7 @@ path = "src/main.rs"
tauri-build.workspace = true tauri-build.workspace = true
[dependencies] [dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" } engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
tauri.workspace = true tauri.workspace = true
tauri-plugin-tracing.workspace = true tauri-plugin-tracing.workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts // file: crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts
// version: 1 // version: 2
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
@@ -10,3 +10,7 @@ export async function getRuntimeLabel(): Promise<string> {
export async function notifyFrontendReady(): Promise<void> { export async function notifyFrontendReady(): Promise<void> {
await invoke("frontend_ready"); await invoke("frontend_ready");
} }
export async function closeMainWindow(): Promise<void> {
await invoke("close_main_window");
}

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts // file: crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts
// version: 1 // version: 2
import init, { ReflexWasmGame } from "../wasm/game_reflex_poc_wasm.js"; import init, { ReflexWasmGame } from "../wasm/game_reflex_poc_wasm.js";
import { tracingDebug, tracingTrace } from "./logging"; import { tracingDebug, tracingTrace } from "./logging";
@@ -37,7 +37,7 @@ function render(game: ReflexWasmGame, canvas: HTMLCanvasElement, context: Canvas
score.value = `Score : ${game.score()}`; score.value = `Score : ${game.score()}`;
} }
export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise<void> { export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise<ReflexWasmGame> {
await init(); await init();
await tracingDebug("Reflex WASM module initialized"); await tracingDebug("Reflex WASM module initialized");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
@@ -64,4 +64,5 @@ export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElem
} }
render(game, canvas, context, score); render(game, canvas, context, score);
window.requestAnimationFrame(frame); window.requestAnimationFrame(frame);
return game;
} }

View File

@@ -1,38 +1,98 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts // file: crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts
// version: 1 // version: 2
import { attachConsole, debug, error, info, trace, warn } from "@fltsci/tauri-plugin-tracing"; import { attachConsole } from "@fltsci/tauri-plugin-tracing";
import { invoke } from "@tauri-apps/api/core";
let detachRustConsole: (() => void) | null = null; export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
export type FrontendLogTarget = "frontend" | "main";
export async function initializeTracing(): Promise<void> { type ConsoleMethod = (...items: unknown[]) => void;
detachRustConsole = await attachConsole();
await info("Reflex Tauri frontend tracing initialized", "games::tauri::reflex::frontend");
}
export async function tracingTrace(message: string): Promise<void> { const originalConsole = {
await trace(message, "games::tauri::reflex::frontend"); trace: console.trace.bind(console),
} debug: console.debug.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
export async function tracingDebug(message: string): Promise<void> { function stringifyItem(item: unknown): string {
await debug(message, "games::tauri::reflex::frontend"); if (item instanceof Error) {
} return item.stack ?? item.message;
}
export async function tracingInfo(message: string): Promise<void> { if (typeof item === "string") {
await info(message, "games::tauri::reflex::frontend"); return item;
} }
try {
export async function tracingWarn(message: string): Promise<void> { const serialized = JSON.stringify(item);
await warn(message, "games::tauri::reflex::frontend"); return serialized ?? String(item);
} } catch {
return String(item);
export async function tracingError(message: string): Promise<void> {
await error(message, "games::tauri::reflex::frontend");
}
export function detachTracingConsole(): void {
if (detachRustConsole !== null) {
detachRustConsole();
detachRustConsole = null;
} }
} }
function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTarget, message: string): Promise<void> {
await invoke("emit_frontend_log", {
payload: {
level,
targetId,
message,
},
});
}
function originalConsoleFor(level: FrontendLogLevel): ConsoleMethod {
return level === "trace"
? originalConsole.trace
: level === "debug"
? originalConsole.debug
: level === "info"
? originalConsole.info
: level === "warn"
? originalConsole.warn
: originalConsole.error;
}
export async function initializeTracing(): Promise<void> {
await attachConsole();
await emitFrontendLog("info", "main", "Reflex Tauri frontend tracing initialized");
}
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTarget, ...items: unknown[]): Promise<void> {
const message = formatMessage(items);
originalConsoleFor(level)(message);
await sendFrontendLog(level, targetId, message);
}
function emitDetached(level: FrontendLogLevel, targetId: FrontendLogTarget, items: unknown[]): void {
const message = formatMessage(items);
originalConsoleFor(level)(message);
void sendFrontendLog(level, targetId, message).catch(caughtError => {
originalConsole.error("Reflex frontend logging bridge failed", caughtError);
});
}
export function tracingTrace(...items: unknown[]): void {
emitDetached("trace", "frontend", items);
}
export function tracingDebug(...items: unknown[]): void {
emitDetached("debug", "frontend", items);
}
export function tracingInfo(...items: unknown[]): void {
emitDetached("info", "frontend", items);
}
export function tracingWarn(...items: unknown[]): void {
emitDetached("warn", "frontend", items);
}
export function tracingError(...items: unknown[]): void {
emitDetached("error", "frontend", items);
}

View File

@@ -1,7 +1,7 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts // file: crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts
// version: 1 // version: 2
import { getRuntimeLabel, notifyFrontendReady } from "./bridge"; import { closeMainWindow, getRuntimeLabel, notifyFrontendReady } from "./bridge";
import { startGame } from "./game"; import { startGame } from "./game";
import { initializeTracing, tracingError, tracingInfo } from "./logging"; import { initializeTracing, tracingError, tracingInfo } from "./logging";
@@ -14,12 +14,23 @@ async function main(): Promise<void> {
throw new Error("Reflex Tauri frontend DOM is incomplete"); throw new Error("Reflex Tauri frontend DOM is incomplete");
} }
runtime.value = await getRuntimeLabel(); runtime.value = await getRuntimeLabel();
await startGame(canvas, score); const game = await startGame(canvas, score);
window.addEventListener("keydown", event => {
if (event.key !== "Escape" || event.repeat) {
return;
}
event.preventDefault();
const shouldExit = game.quit_requested_escape();
tracingInfo(`quit requested source=escape decision=${shouldExit ? "exit" : "continue"}`);
if (shouldExit) {
void closeMainWindow().catch(caughtError => tracingError("Tauri close command failed", caughtError));
}
});
await notifyFrontendReady(); await notifyFrontendReady();
await tracingInfo("Reflex Tauri frontend started"); tracingInfo("Reflex Tauri frontend started");
} }
void main().catch(async caughtError => { void main().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError); const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError);
await tracingError(`Reflex Tauri frontend startup failed: ${message}`); tracingError(`Reflex Tauri frontend startup failed: ${message}`);
}); });

View File

@@ -1,7 +1,7 @@
{ {
"name": "game-reflex-poc-tauri", "name": "game-reflex-poc-tauri",
"private": true, "private": true,
"version": "0.1.0-1-alpha.2.fix.2", "version": "0.1.0-1-alpha.2.fix.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/src/lib.rs // file: crates/apps/game-reflex-poc-tauri/src/lib.rs
// version: 2 // version: 3
//! Tauri desktop application facade for the Reflex WebAssembly POC. //! Tauri desktop application facade for the Reflex WebAssembly POC.
@@ -15,5 +15,7 @@ pub use self::tauri::run;
/// Records that the Vite/TypeScript frontend reached its ready state. /// Records that the Vite/TypeScript frontend reached its ready state.
pub(crate) use self::runtime::record_frontend_ready; pub(crate) use self::runtime::record_frontend_ready;
/// Records one accepted native window close request.
pub(crate) use self::runtime::record_window_close;
/// Returns the runtime label exposed by the Tauri bridge. /// Returns the runtime label exposed by the Tauri bridge.
pub(crate) use self::runtime::runtime_label; pub(crate) use self::runtime::runtime_label;

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/src/runtime.rs // file: crates/apps/game-reflex-poc-tauri/src/runtime.rs
// version: 1 // version: 2
/// Stable label for the current Tauri/WASM runtime combination. /// Stable label for the current Tauri/WASM runtime combination.
pub(crate) fn runtime_label() -> &'static str { pub(crate) fn runtime_label() -> &'static str {
@@ -16,3 +16,14 @@ pub(crate) fn record_frontend_ready() {
); );
return; return;
} }
/// Records one accepted native window close request.
pub(crate) fn record_window_close(source: &str) {
tracing::info!(
target: "games::tauri::reflex",
action = "window_close",
source = source,
"Reflex Tauri window close accepted"
);
return;
}

View File

@@ -1,5 +1,15 @@
// file: crates/apps/game-reflex-poc-tauri/src/tauri.rs // file: crates/apps/game-reflex-poc-tauri/src/tauri.rs
// version: 1 // version: 2
//! Tauri runtime assembly and Web/Rust commands for the Reflex POC.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct FrontendLogPayload {
level: String,
target_id: String,
message: String,
}
#[tauri::command] #[tauri::command]
fn get_runtime_label() -> String { fn get_runtime_label() -> String {
@@ -12,6 +22,47 @@ fn frontend_ready() {
return; return;
} }
#[tauri::command]
fn emit_frontend_log(payload: FrontendLogPayload) -> std::result::Result<(), String> {
let target_id = payload.target_id.trim();
if target_id != "frontend" && target_id != "main" {
return std::result::Result::Err("unsupported frontend log target".to_string());
}
let message = payload.message.as_str();
return match payload.level.trim().to_ascii_lowercase().as_str() {
"trace" => {
tracing::trace!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"debug" => {
tracing::debug!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"info" => {
tracing::info!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"warn" => {
tracing::warn!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"error" => {
tracing::error!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
_ => std::result::Result::Err("unsupported frontend log level".to_string()),
};
}
#[tauri::command]
fn close_main_window(window: tauri::WebviewWindow) -> std::result::Result<(), String> {
crate::record_window_close("game_quit_decision");
return match window.close() {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>(); let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tracing_plugin); return builder.plugin(tracing_plugin);
@@ -19,11 +70,16 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for command dispatch. #[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![get_runtime_label, frontend_ready]); return builder.invoke_handler(tauri::generate_handler![get_runtime_label, frontend_ready, emit_frontend_log, close_main_window]);
} }
/// Runs the Reflex Tauri desktop application. /// Runs the Reflex Tauri desktop application.
pub fn run() -> std::result::Result<(), String> { pub fn run() -> std::result::Result<(), String> {
let logging_guard = game_logging_lib::init_console_tracing();
let _logging_guard = match logging_guard {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::info!( tracing::info!(
target: "games::tauri::reflex", target: "games::tauri::reflex",
action = "runtime_start", action = "runtime_start",
@@ -33,7 +89,22 @@ pub fn run() -> std::result::Result<(), String> {
let builder = configure_plugins(builder); let builder = configure_plugins(builder);
let builder = configure_commands(builder); let builder = configure_commands(builder);
return match builder.run(tauri::generate_context!()) { return match builder.run(tauri::generate_context!()) {
std::result::Result::Ok(()) => std::result::Result::Ok(()), std::result::Result::Ok(()) => {
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), tracing::info!(
target: "games::tauri::reflex",
action = "runtime_stop",
"Reflex Tauri runtime stopped"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => {
tracing::error!(
target: "games::tauri::reflex",
action = "runtime_error",
error = error.to_string(),
"Reflex Tauri runtime failed"
);
std::result::Result::Err(error.to_string())
},
}; };
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Reflex POC Tauri", "productName": "Reflex POC Tauri",
"version": "0.1.0-1-alpha.2.fix.2", "version": "0.1.0-1-alpha.2.fix.3",
"identifier": "com.sasedev.games.reflex.tauri", "identifier": "com.sasedev.games.reflex.tauri",
"build": { "build": {
"beforeDevCommand": { "beforeDevCommand": {

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-wasm/src/runtime.rs // file: crates/apps/game-reflex-poc-wasm/src/runtime.rs
// version: 1 // version: 2
/// WebAssembly-owned Reflex game state consumed by the Tauri WebView. /// WebAssembly-owned Reflex game state consumed by the Tauri WebView.
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]
@@ -33,6 +33,12 @@ impl ReflexWasmGame {
return; return;
} }
/// Routes the Escape key through the shared game's quit policy.
pub fn quit_requested_escape(&mut self) -> bool {
let request = engine_v1_common::QuitRequest::new(engine_v1_common::QuitSource::Escape);
return engine_v1_common::EngineGame::quit_requested(&mut self.state, request) == engine_v1_common::QuitDecision::Exit;
}
/// Returns the current gameplay score. /// Returns the current gameplay score.
pub fn score(&self) -> u32 { pub fn score(&self) -> u32 {
return self.state.score().min(u32::MAX as u64) as u32; return self.state.score().min(u32::MAX as u64) as u32;

View File

@@ -0,0 +1,98 @@
<!-- file: deltas/0.1.0/1-alpha.2.fix.3.md -->
<!-- version: 1 -->
# Delta 0.1.0-1-alpha.2.fix.3
## Base
Base déclarée : `0.1.0-1-alpha.2.fix.2`, gates techniques propres et smoke Tauri fonctionnel.
Deux défauts fonctionnels restent ouverts :
- aucun événement `tracing` n'est visible dans le terminal ;
- `Escape` ne produit aucune action dans la variante Tauri.
## Logging
La variante Tauri dépend désormais de `game-logging-lib` et initialise :
```text
game_logging_lib::init_console_tracing()
```
avant le premier événement `tracing`.
Le frontend utilise son wrapper TypeScript :
```text
module TS
-> logging.ts
-> invoke("emit_frontend_log")
-> tauri.rs
-> tracing
-> game-logging-lib subscriber
-> terminal
```
Le plugin `tauri-plugin-tracing` reste installé et `attachConsole()` conserve la direction Rust vers console WebView.
## Escape
La crate WASM expose :
```text
quit_requested_escape() -> bool
```
Cette méthode appelle le contrat moteur :
```text
EngineGame::quit_requested(QuitRequest::new(QuitSource::Escape))
```
Le frontend invoque `close_main_window` seulement si la décision vaut `Exit`.
## Validation
```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 history
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p game-logging-lib --all-targets --all-features
cargo test -p game-reflex-poc --all-targets --all-features
cargo test -p game-reflex-poc-wasm --all-targets --all-features
```
## Gate WASM
```bash
python3 scripts/build_reflex_tauri_wasm.py
```
## Smoke Tauri
```bash
cd crates/apps/game-reflex-poc-tauri
cargo tauri dev
```
Des événements Rust et frontend doivent apparaître dans le terminal, notamment le démarrage du runtime et le ready frontend.
Presser ensuite `Escape`.
Attendus :
- événement frontend indiquant `source=escape` et la décision ;
- événement Rust `window close accepted` ;
- fermeture de la fenêtre ;
- retour propre au shell.
## Transition
Si les gates et le smoke passent, `0.1.0-1-alpha.2.fix.3` valide le POC Tauri/WASM avec logging visible et politique de sortie commune.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/development/008-WASM_TAURI_POC.md --> <!-- file: docs/development/008-WASM_TAURI_POC.md -->
<!-- version: 3 --> <!-- version: 4 -->
# POC WebAssembly embarqué dans Tauri # POC WebAssembly embarqué dans Tauri
@@ -145,3 +145,27 @@ Le logging est unifié avec `tracing` :
- capability `tracing:default`. - capability `tracing:default`.
Les événements frontend utiles sont donc remontés dans le même système de traces que le backend Rust. Les événements frontend utiles sont donc remontés dans le même système de traces que le backend Rust.
## Logging fix.3
Le plugin Tauri ne remplace pas l'initialisation du subscriber `tracing`.
`game-logging-lib::init_console_tracing()` installe le subscriber console avant le premier événement Tauri.
Le frontend possède un wrapper TypeScript unique qui invoque `emit_frontend_log`; Rust valide le niveau et la cible puis réémet l'événement avec `tracing`.
`attachConsole()` reste activé pour rendre également les événements Rust observables dans la console WebView.
## Escape fix.3
`Escape` suit désormais le contrat moteur commun :
```text
keydown Escape
-> ReflexWasmGame::quit_requested_escape()
-> EngineGame::quit_requested(QuitSource::Escape)
-> QuitDecision
-> close_main_window uniquement si Exit
```
Une future politique de pause/confirmation pourra retourner `Continue` sans modifier le bridge Tauri.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_PROJECT.md --> <!-- file: docs/rules/RULES_PROJECT.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Règles spécifiques games.sasedev # Règles spécifiques games.sasedev
@@ -67,3 +67,6 @@
- **GAME-PLATFORM-009** — Une adaptation WebAssembly réutilisable est une crate dédiée distincte de la crate Tauri. - **GAME-PLATFORM-009** — Une adaptation WebAssembly réutilisable est une crate dédiée distincte de la crate Tauri.
- **GAME-PLATFORM-010** — Dans une app Tauri, `lib.rs` reste une façade/reexport ; `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires. - **GAME-PLATFORM-010** — Dans une app Tauri, `lib.rs` reste une façade/reexport ; `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires.
- **GAME-PLATFORM-011** — Les traces frontend Tauri passent par `@fltsci/tauri-plugin-tracing` vers `tauri-plugin-tracing`/`tracing`; les `console.*` applicatifs directs sont interdits hors fallback interne. - **GAME-PLATFORM-011** — Les traces frontend Tauri passent par `@fltsci/tauri-plugin-tracing` vers `tauri-plugin-tracing`/`tracing`; les `console.*` applicatifs directs sont interdits hors fallback interne.
- **GAME-PLATFORM-012** — Le plugin tracing Tauri ne remplace pas l'initialisation du subscriber Rust ; une app Tauri initialise le runtime de logging partagé avant son premier événement `tracing`.
- **GAME-PLATFORM-013** — Les demandes de sortie Tauri sont évaluées par la politique `EngineGame::quit_requested` avant toute fermeture native.