Files
games/scripts/audit_distribution_layout.py
2026-09-22 08:49:40 +02:00

589 lines
35 KiB
Python

#!/usr/bin/env python3
# file: scripts/audit_distribution_layout.py
# version: 24
"""Audit the static distribution layout expected by supported POCs."""
from __future__ import annotations
import argparse
import pathlib
import sys
FORBIDDEN_PATHS = (
"builds",
"Android/gradlew",
"Android/gradlew.bat",
"Android/gradle/wrapper/gradle-wrapper.jar",
"Android/gradle/wrapper/gradle-wrapper.properties",
"Android/game-reflex-poc/src/main/jniLibs",
"Android/game-snake-poc/src/main/jniLibs",
"scripts/build_android_rust.py",
)
REQUIRED_PATHS = (
"crates/apps/game-realtime-transport-fallback-smoke/Cargo.toml",
"crates/apps/game-realtime-transport-fallback-smoke/src/main.rs",
"crates/apps/game-realtime-transport-fallback-smoke/unit_tests/fallback.rs",
"crates/apps/game-realtime-transport-measure/Cargo.toml",
"crates/apps/game-realtime-transport-measure/src/main.rs",
"crates/apps/game-realtime-websocket-smoke/Cargo.toml",
"crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml",
"crates/apps/game-realtime-webtransport-datagram-smoke/Cargo.toml",
"crates/apps/game-realtime-webtransport-datagram-smoke/src/main.rs",
"crates/common/game-realtime-webtransport-lib/tests/datagrams.rs",
"crates/apps/game-realtime-webtransport-browser-smoke/src/lib.rs",
"crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs",
"crates/apps/game-reflex-poc-desktop/Cargo.toml",
"crates/apps/game-snake-poc-desktop/Cargo.toml",
"crates/apps/game-reflex-poc-tauri/Cargo.toml",
"crates/apps/game-reflex-poc-tauri/package.json",
"crates/apps/game-reflex-poc-tauri/tsconfig.json",
"crates/apps/game-reflex-poc-tauri/vite.config.ts",
"crates/apps/game-reflex-poc-tauri/tauri.conf.json",
"crates/apps/game-reflex-poc-tauri/capabilities/default.json",
"crates/apps/game-reflex-poc-tauri/icons/icon.png",
"crates/apps/game-reflex-poc-tauri/frontend/index.html",
"crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts",
"crates/apps/game-reflex-poc-wasm/Cargo.toml",
"crates/apps/game-snake-poc-wasm/Cargo.toml",
"crates/apps/game-snake-poc-tauri/Cargo.toml",
"crates/apps/game-snake-poc-tauri/README.md",
"crates/apps/game-snake-poc-tauri/USAGE.md",
"crates/apps/game-snake-poc-tauri/package.json",
"crates/apps/game-snake-poc-tauri/tsconfig.json",
"crates/apps/game-snake-poc-tauri/vite.config.ts",
"crates/apps/game-snake-poc-tauri/tauri.conf.json",
"crates/apps/game-snake-poc-tauri/capabilities/default.json",
"crates/apps/game-snake-poc-tauri/icons/icon.png",
"crates/apps/game-snake-poc-tauri/frontend/main.html",
"crates/apps/game-snake-poc-tauri/frontend/css/main.css",
"crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts",
"crates/apps/game-snake-poc-tauri/frontend/ts/game.ts",
"crates/apps/game-snake-poc-tauri/frontend/ts/input.ts",
"crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts",
"crates/apps/game-snake-poc-tauri/frontend/ts/main.ts",
"crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts",
"Web/game-realtime-webtransport-browser-smoke/package.json",
"Web/game-realtime-webtransport-browser-smoke/tsconfig.json",
"Web/game-realtime-webtransport-browser-smoke/vite.config.ts",
"Web/game-realtime-webtransport-browser-smoke/frontend/main.html",
"Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts",
"Web/game-snake-poc/package.json",
"Web/game-snake-poc/tsconfig.json",
"Web/game-snake-poc/vite.config.ts",
"Web/game-snake-poc/frontend/main.html",
"Web/game-snake-poc/frontend/sass/_simplebar.scss",
"Web/game-snake-poc/frontend/sass/main.scss",
"Web/game-snake-poc/frontend/ts/main.ts",
"Web/game-snake-poc/frontend/ts/assets.ts",
"Web/game-snake-poc/frontend/ts/lifecycle.ts",
"Web/game-snake-poc/frontend/ts/logging.ts",
"Web/game-snake-poc/frontend/ts/provenance.ts",
"Android/game-reflex-poc/build.gradle",
"Android/game-snake-poc/build.gradle",
"Android/settings.gradle",
"Android/gradle/sasedev-rust-android.gradle",
"scripts/build_reflex_tauri_wasm.py",
"assets/common",
"assets/game-reflex-poc",
"assets/game-snake-poc",
)
def audit_snake_web_shell(root: pathlib.Path) -> list[str]:
"""Validate the KSP-derived bounded SimpleBar shell contract."""
violations: list[str] = []
html_path = root / "Web/game-snake-poc/frontend/main.html"
app_scss_path = root / "Web/game-snake-poc/frontend/sass/_app.scss"
main_ts_path = root / "Web/game-snake-poc/frontend/ts/main.ts"
if not html_path.exists() or not app_scss_path.exists() or not main_ts_path.exists():
return violations
html = html_path.read_text(encoding="utf-8")
app_scss = app_scss_path.read_text(encoding="utf-8")
main_ts = main_ts_path.read_text(encoding="utf-8")
required_html = (
'<main class="app-main container-fluid p-0">',
'<div class="row g-0 h-100 flex-nowrap">',
'<section class="col app-content app-scrollable h-100" data-simplebar>',
)
for fragment in required_html:
if fragment not in html:
violations.append(f"DIST-LAYOUT-003: Snake Web shell missing required HTML contract: {fragment}")
if '<main class="app-main container-fluid p-0" data-simplebar>' in html:
violations.append("DIST-LAYOUT-004: data-simplebar must be attached to the bounded app-content zone, not app-main")
required_scss = (
".app-scrollable {",
"height: 100%;",
"max-height: 100%;",
)
for fragment in required_scss:
if fragment not in app_scss:
violations.append(f"DIST-LAYOUT-005: Snake Web shell missing required Sass contract: {fragment}")
required_ts = (
'import ResizeObserver from "resize-observer-polyfill";',
'import "simplebar";',
').ResizeObserver = ResizeObserver;',
)
for fragment in required_ts:
if fragment not in main_ts:
violations.append(f"DIST-LAYOUT-006: Snake Web shell missing SimpleBar bootstrap contract: {fragment}")
return violations
def audit_snake_web_integration(root: pathlib.Path) -> list[str]:
"""Validate assets, lifecycle, logging and provenance wiring for the Snake Web POC."""
violations: list[str] = []
package_path = root / "Web/game-snake-poc/package.json"
vite_path = root / "Web/game-snake-poc/vite.config.ts"
main_path = root / "Web/game-snake-poc/frontend/ts/main.ts"
lifecycle_path = root / "Web/game-snake-poc/frontend/ts/lifecycle.ts"
provenance_path = root / "Web/game-snake-poc/frontend/ts/provenance.ts"
wasm_manifest_path = root / "crates/apps/game-snake-poc-wasm/Cargo.toml"
wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs"
if not all(path.exists() for path in (package_path, vite_path, main_path, lifecycle_path, provenance_path, wasm_manifest_path, wasm_runtime_path)):
return violations
package = package_path.read_text(encoding="utf-8")
vite = vite_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
lifecycle = lifecycle_path.read_text(encoding="utf-8")
provenance = provenance_path.read_text(encoding="utf-8")
wasm_manifest = wasm_manifest_path.read_text(encoding="utf-8")
wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8")
required_fragments = (
(package, '"vite-plugin-static-copy"', "DIST-LAYOUT-007: Snake Web package must declare static asset packaging"),
(vite, 'dest: "common/data", rename: { stripBase: true }', "DIST-LAYOUT-008: common Web assets must retain the common/data namespace without absolute source prefixes"),
(vite, 'dest: "game/data", rename: { stripBase: true }', "DIST-LAYOUT-009: game Web assets must retain the game/data namespace without absolute source prefixes"),
(main, 'loadSnakeWebAssets()', "DIST-LAYOUT-010: Snake Web startup must load runtime asset probes"),
(main, 'bindBrowserLifecycle(session, stage)', "DIST-LAYOUT-011: Snake Web startup must bind browser lifecycle"),
(lifecycle, '"visibilitychange"', "DIST-LAYOUT-012: Snake Web lifecycle must observe visibility changes"),
(lifecycle, '"pagehide"', "DIST-LAYOUT-013: Snake Web lifecycle must observe pagehide"),
(lifecycle, 'ResizeObserver', "DIST-LAYOUT-014: Snake Web lifecycle must observe resize"),
(provenance, '"keyboard-mouse"', "DIST-LAYOUT-015: Snake Web provenance must distinguish keyboard/mouse input"),
(provenance, '"touch"', "DIST-LAYOUT-016: Snake Web provenance must distinguish touch input"),
(wasm_manifest, 'engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }', "DIST-LAYOUT-017: Snake WASM adapter must consume the platform provenance API"),
(wasm_runtime, 'PlatformFamily::Web', "DIST-LAYOUT-018: Snake WASM provenance must declare Web platform family"),
(wasm_runtime, 'ExecutionModel::Wasm', "DIST-LAYOUT-019: Snake WASM provenance must declare WASM execution"),
(wasm_runtime, 'RuntimeHost::Browser', "DIST-LAYOUT-020: Snake WASM provenance must declare browser host"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
return violations
def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]:
"""Validate the playable Snake Tauri Android reference POC boundary."""
violations: list[str] = []
app_root = root / "crates/apps/game-snake-poc-tauri"
cargo_path = app_root / "Cargo.toml"
package_path = app_root / "package.json"
tauri_path = app_root / "tauri.conf.json"
tauri_rust_path = app_root / "src/tauri.rs"
build_script_path = app_root / "build.rs"
html_path = app_root / "frontend/main.html"
main_path = app_root / "frontend/ts/main.ts"
assets_path = app_root / "frontend/ts/assets.ts"
game_path = app_root / "frontend/ts/game.ts"
input_path = app_root / "frontend/ts/input.ts"
provenance_path = app_root / "frontend/ts/provenance.ts"
lifecycle_path = app_root / "frontend/ts/lifecycle.ts"
logging_path = app_root / "frontend/ts/logging.ts"
vite_path = app_root / "vite.config.ts"
wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs"
paths = (
cargo_path,
package_path,
tauri_path,
tauri_rust_path,
build_script_path,
html_path,
main_path,
assets_path,
game_path,
input_path,
provenance_path,
lifecycle_path,
logging_path,
vite_path,
wasm_runtime_path,
)
if not all(path.exists() for path in paths):
return violations
cargo = cargo_path.read_text(encoding="utf-8")
package = package_path.read_text(encoding="utf-8")
tauri = tauri_path.read_text(encoding="utf-8")
tauri_rust = tauri_rust_path.read_text(encoding="utf-8")
build_script = build_script_path.read_text(encoding="utf-8")
html = html_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
assets = assets_path.read_text(encoding="utf-8")
game = game_path.read_text(encoding="utf-8")
input_module = input_path.read_text(encoding="utf-8")
provenance = provenance_path.read_text(encoding="utf-8")
lifecycle = lifecycle_path.read_text(encoding="utf-8")
logging = logging_path.read_text(encoding="utf-8")
vite = vite_path.read_text(encoding="utf-8")
wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8")
required_fragments = (
(cargo, 'game-logging-lib = { path = "../../common/game-logging-lib" }', "DIST-LAYOUT-021: Snake Tauri host must use shared Rust logging"),
(cargo, 'tauri-plugin-tracing.workspace = true', "DIST-LAYOUT-022: Snake Tauri host must enable the tracing plugin"),
(package, '"wasm:dev": "cargo build -p game-snake-poc-wasm', "DIST-LAYOUT-023: Snake Tauri dev hook must build the shared Snake WASM adapter"),
(package, 'wasm-bindgen ../../../../builds/sasedev-games/', "DIST-LAYOUT-024: Snake Tauri bindings must be generated outside the repository"),
(package, '"dev": "npm run wasm:dev && vite"', "DIST-LAYOUT-036: Snake Tauri dev hook must leave Android generation to Tauri"),
(package, '"build": "npm run wasm:build && tsc && vite build"', "DIST-LAYOUT-037: Snake Tauri build hook must leave Android generation to Tauri"),
(package, '"vite-plugin-static-copy"', "DIST-LAYOUT-043: Snake Tauri package must declare static asset packaging"),
(build_script, "tauri_build::build();", "DIST-LAYOUT-038: Snake Tauri build script must delegate Android integration to tauri-build"),
(tauri, '"script": "npm run dev"', "DIST-LAYOUT-025: Tauri must own the frontend development hook"),
(tauri, '"script": "npm run build"', "DIST-LAYOUT-026: Tauri must own the frontend production hook"),
(tauri_rust, '#[tauri::mobile_entry_point]', "DIST-LAYOUT-027: Snake Tauri run entry point must support Tauri mobile"),
(tauri_rust, 'tauri_plugin_tracing::Builder::new()', "DIST-LAYOUT-028: Snake Tauri runtime must install the tracing plugin"),
(html, '<canvas id="game"', "DIST-LAYOUT-044: Snake Tauri reference POC must expose the Canvas"),
(html, 'data-direction="up"', "DIST-LAYOUT-045: Snake Tauri reference POC must expose touch direction controls"),
(main, 'loadSnakeTauriAssets()', "DIST-LAYOUT-046: Snake Tauri startup must load canonical runtime assets"),
(main, 'startGame(canvas, score, length, provenance, descriptor)', "DIST-LAYOUT-047: Snake Tauri startup must run the shared WASM game through Canvas"),
(main, 'bindInputs(session, buttons)', "DIST-LAYOUT-048: Snake Tauri startup must bind host inputs"),
(main, 'bindTauriLifecycle(session, stage)', "DIST-LAYOUT-059: Snake Tauri startup must bind WebView lifecycle"),
(assets, './common/data/runtime.json', "DIST-LAYOUT-049: Snake Tauri asset probe must load common runtime metadata"),
(assets, './game/data/game.json', "DIST-LAYOUT-050: Snake Tauri asset probe must load Snake metadata"),
(game, 'window.requestAnimationFrame(frame)', "DIST-LAYOUT-051: Snake Tauri Canvas host must schedule its fixed-step session"),
(game, 'game.rectangle_count()', "DIST-LAYOUT-052: Snake Tauri Canvas host must render the shared WASM scene"),
(input_module, '"pointerdown"', "DIST-LAYOUT-053: Snake Tauri input must support pointer/touch controls"),
(input_module, '"keydown"', "DIST-LAYOUT-054: Snake Tauri input must retain keyboard controls when available"),
(provenance, '"touch"', "DIST-LAYOUT-055: Snake Tauri provenance must observe touch input"),
(provenance, '"keyboard-mouse"', "DIST-LAYOUT-056: Snake Tauri provenance must observe keyboard/mouse input"),
(lifecycle, '"visibilitychange"', "DIST-LAYOUT-060: Snake Tauri lifecycle must observe visibility changes"),
(lifecycle, '"pagehide"', "DIST-LAYOUT-061: Snake Tauri lifecycle must observe pagehide"),
(lifecycle, '"pageshow"', "DIST-LAYOUT-062: Snake Tauri lifecycle must observe pageshow"),
(lifecycle, "ResizeObserver", "DIST-LAYOUT-063: Snake Tauri lifecycle must observe stage resize/orientation"),
(game, "pause(): void;", "DIST-LAYOUT-064: Snake Tauri session must expose pause semantics"),
(game, "resume(): void;", "DIST-LAYOUT-065: Snake Tauri session must expose resume semantics"),
(game, "accumulator = 0;", "DIST-LAYOUT-066: Snake Tauri resume/pause must reset accumulated elapsed time"),
(vite, 'dest: "common/data", rename: { stripBase: true }', "DIST-LAYOUT-057: Snake Tauri common assets must retain the common/data namespace"),
(vite, 'dest: "game/data", rename: { stripBase: true }', "DIST-LAYOUT-058: Snake Tauri game assets must retain the game/data namespace"),
(vite, 'host: devHost || "127.0.0.1"', "DIST-LAYOUT-068: Snake Tauri HMR must use a loopback-compatible host for ADB forwarding"),
(vite, 'port: 1436,', "DIST-LAYOUT-069: Snake Tauri Vite WebSocket must share port 1436 with the dev server"),
(vite, 'clientPort: 1436,', "DIST-LAYOUT-070: Snake Tauri HMR client must use forwarded port 1436"),
(logging, '@fltsci/tauri-plugin-tracing', "DIST-LAYOUT-030: Snake Tauri frontend logs must use the tracing bridge"),
(wasm_runtime, '"tauri-webview" => Some(engine_v1_platform_api::RuntimeHost::TauriWebView)', "DIST-LAYOUT-031: Snake WASM adapter must accept the Tauri WebView host"),
(wasm_runtime, '"android" => Some(engine_v1_platform_api::PlatformFamily::Android)', "DIST-LAYOUT-032: Snake WASM adapter must accept Android provenance"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
forbidden_fragments = (
(package, "python", "DIST-LAYOUT-033: Snake Tauri build hooks must not use Python"),
(package, "android:prepare", "DIST-LAYOUT-039: Snake Tauri build hooks must not patch generated Android Gradle files"),
(build_script, "gen/android", "DIST-LAYOUT-040: Snake Tauri build script must not mutate generated Android Gradle files"),
(tauri, "python", "DIST-LAYOUT-034: Snake Tauri configuration must not use Python build orchestration"),
(main, "console.", "DIST-LAYOUT-035: Snake Tauri application frontend must not log directly through console.*"),
(main, "onbackbuttonpress", "DIST-LAYOUT-067: Snake Tauri reference POC must leave Android Back to the system while Snake quit policy is Exit"),
(lifecycle, "onbackbuttonpress", "DIST-LAYOUT-067: Snake Tauri reference POC must leave Android Back to the system while Snake quit policy is Exit"),
(vite, "1437", "DIST-LAYOUT-071: Snake Tauri dev/HMR must not require a second unforwarded WebSocket port"),
)
for content, fragment, message in forbidden_fragments:
if fragment in content.lower():
violations.append(message)
return violations
def audit_snake_tauri_generated_android(root: pathlib.Path) -> list[str]:
"""Reject a locally generated Snake Android tree that drifted from Tauri 2.11.x."""
violations: list[str] = []
android_root = root / "crates/apps/game-snake-poc-tauri/gen/android"
if not android_root.exists():
return violations
wrapper_path = android_root / "gradle/wrapper/gradle-wrapper.properties"
build_path = android_root / "build.gradle.kts"
if not wrapper_path.exists() or not build_path.exists():
violations.append(
"DIST-LAYOUT-041: generated Snake Tauri Android tree is incomplete; remove gen/android and rerun cargo tauri android init"
)
return violations
wrapper = wrapper_path.read_text(encoding="utf-8")
build = build_path.read_text(encoding="utf-8")
expected_fragments = (
(wrapper, "gradle-8.14.3-bin.zip", "Gradle 8.14.3"),
(build, 'classpath("com.android.tools.build:gradle:8.11.0")', "Android Gradle Plugin 8.11.0"),
(build, 'classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")', "Kotlin Gradle Plugin 1.9.25"),
)
for content, fragment, label in expected_fragments:
if fragment not in content:
violations.append(
f"DIST-LAYOUT-042: generated Snake Tauri Android tree does not match Tauri 2.11.x {label}; "
"remove gen/android and rerun cargo tauri android init under JDK 17"
)
return violations
def audit_webtransport_browser_smoke(root: pathlib.Path) -> list[str]:
"""Validate the technical real-browser WebTransport smoke boundary."""
violations: list[str] = []
package_path = root / "Web/game-realtime-webtransport-browser-smoke/package.json"
tsconfig_path = root / "Web/game-realtime-webtransport-browser-smoke/tsconfig.json"
vite_path = root / "Web/game-realtime-webtransport-browser-smoke/vite.config.ts"
main_path = root / "Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts"
rust_manifest_path = root / "crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml"
rust_main_path = root / "crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs"
if not all(path.exists() for path in (package_path, tsconfig_path, vite_path, main_path, rust_manifest_path, rust_main_path)):
return violations
package = package_path.read_text(encoding="utf-8")
tsconfig = tsconfig_path.read_text(encoding="utf-8")
vite = vite_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
rust_manifest = rust_manifest_path.read_text(encoding="utf-8")
rust_main = rust_main_path.read_text(encoding="utf-8")
required_fragments = (
(package, 'cargo build -p game-realtime-webtransport-browser-smoke --lib --target wasm32-unknown-unknown', "DIST-LAYOUT-077: browser WebTransport smoke must build its Rust WASM adapter"),
(package, 'wasm-bindgen ../../../builds/sasedev-games/', "DIST-LAYOUT-078: browser WebTransport smoke bindings must be generated outside the repository"),
(tsconfig, '../../../builds/sasedev-games/game-realtime-webtransport-browser-smoke/wasm/game_realtime_webtransport_browser_smoke.d.ts', "DIST-LAYOUT-086: browser WebTransport smoke TypeScript declarations must resolve from the external build root"),
(vite, 'host: "127.0.0.1"', "DIST-LAYOUT-079: browser WebTransport smoke Vite host must stay on loopback"),
(vite, 'port: 1435,', "DIST-LAYOUT-080: browser WebTransport smoke Vite port must stay deterministic"),
(main, 'window.isSecureContext', "DIST-LAYOUT-081: browser WebTransport smoke must verify secure-context availability"),
(main, '"WebTransport" in window', "DIST-LAYOUT-082: browser WebTransport smoke must verify WebTransport availability"),
(main, 'run_browser_smoke(', "DIST-LAYOUT-083: browser WebTransport smoke must execute the Rust WASM path"),
(rust_manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-084: browser WebTransport smoke adapter must consume the shared backend"),
(rust_main, 'game-realtime-webtransport-browser-smoke: PASS', "DIST-LAYOUT-085: browser WebTransport smoke native peer must expose a deterministic PASS verdict"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
return violations
def audit_realtime_fallback_smoke(root: pathlib.Path) -> list[str]:
"""Validate the technical WebTransport-first/WebSocket fallback composition boundary."""
violations: list[str] = []
manifest_path = root / "crates/apps/game-realtime-transport-fallback-smoke/Cargo.toml"
main_path = root / "crates/apps/game-realtime-transport-fallback-smoke/src/main.rs"
unit_path = root / "crates/apps/game-realtime-transport-fallback-smoke/unit_tests/fallback.rs"
if not manifest_path.exists() or not main_path.exists() or not unit_path.exists():
return violations
manifest = manifest_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
unit = unit_path.read_text(encoding="utf-8")
required_fragments = (
(manifest, 'game-realtime-websocket-lib = { path = "../../common/game-realtime-websocket-lib" }', "DIST-LAYOUT-087: fallback smoke must consume the WebSocket backend directly at composition level"),
(manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-088: fallback smoke must consume the WebTransport backend directly at composition level"),
(main, 'kind == game_realtime_transport_lib::TransportErrorKind::Timeout || kind == game_realtime_transport_lib::TransportErrorKind::Io', "DIST-LAYOUT-089: fallback smoke classification must remain limited to Timeout/Io"),
(unit, 'assert!(!super::is_fallback_eligible(game_realtime_transport_lib::TransportErrorKind::Connect));', "DIST-LAYOUT-090: ambiguous WebTransport Connect failures must not trigger fallback"),
(main, 'game-realtime-transport-fallback-smoke: PASS', "DIST-LAYOUT-091: fallback smoke must expose a deterministic PASS verdict"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
forbidden_fragments = (
"engine-v1-",
"game-reflex-poc",
"game-snake-poc",
)
for fragment in forbidden_fragments:
if fragment in manifest:
violations.append(f"DIST-LAYOUT-092: fallback smoke must not depend on engine/gameplay package: {fragment}")
return violations
def audit_realtime_transport_measure(root: pathlib.Path) -> list[str]:
"""Validate the bounded realtime transport characterization boundary."""
violations: list[str] = []
manifest_path = root / "crates/apps/game-realtime-transport-measure/Cargo.toml"
main_path = root / "crates/apps/game-realtime-transport-measure/src/main.rs"
study_path = root / "docs/studies/027-V0_3_5_REALTIME_TRANSPORT_MEASUREMENT.md"
required = (manifest_path, main_path, study_path)
if not all(path.exists() for path in required):
return violations
manifest = manifest_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
study = study_path.read_text(encoding="utf-8")
required_fragments = (
(manifest, 'game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" }', "DIST-LAYOUT-102: measurement tool must use the transport-neutral contract"),
(manifest, 'game-realtime-websocket-lib = { path = "../../common/game-realtime-websocket-lib" }', "DIST-LAYOUT-103: measurement tool must consume the WebSocket backend directly"),
(manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-104: measurement tool must consume the WebTransport backend directly"),
(main, 'const ESTABLISHMENT_SAMPLES: usize = 8;', "DIST-LAYOUT-105: measurement tool must keep establishment samples bounded"),
(main, 'const RTT_SAMPLES: usize = 128;', "DIST-LAYOUT-106: measurement tool must keep RTT samples bounded"),
(main, 'const THROUGHPUT_MESSAGES: usize = 128;', "DIST-LAYOUT-107: measurement tool must keep throughput messages bounded"),
(main, 'const IN_FLIGHT_MESSAGES: usize = 64;', "DIST-LAYOUT-108: measurement tool must keep the application in-flight window bounded"),
(main, 'MEASURE transport=webtransport-datagram metric=bounded_burst', "DIST-LAYOUT-109: measurement tool must report datagrams separately from the reliable contract"),
(main, 'CONCLUSION webtransport=retain scope=second-backend', "DIST-LAYOUT-110: measurement tool must expose the provisional retain conclusion without declaring a speed winner"),
(main, 'game-realtime-transport-measure: PASS', "DIST-LAYOUT-111: measurement tool must expose a deterministic PASS verdict"),
(study, 'Cette conclusion **ne signifie pas** que WebTransport est déclaré plus rapide que WebSocket.', "DIST-LAYOUT-112: measurement study must forbid interpreting retain as a performance winner"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
for fragment in ("engine-v1-", "game-reflex-poc", "game-snake-poc"):
if fragment in manifest:
violations.append(f"DIST-LAYOUT-113: measurement tool must not depend on engine/gameplay package: {fragment}")
return violations
def audit_webtransport_datagram_smoke(root: pathlib.Path) -> list[str]:
"""Validate the isolated backend-specific WebTransport datagram proof."""
violations: list[str] = []
manifest_path = root / "crates/apps/game-realtime-webtransport-datagram-smoke/Cargo.toml"
main_path = root / "crates/apps/game-realtime-webtransport-datagram-smoke/src/main.rs"
native_path = root / "crates/common/game-realtime-webtransport-lib/src/webtransport.rs"
wasm_path = root / "crates/common/game-realtime-webtransport-lib/src/webtransport_wasm.rs"
common_path = root / "crates/common/game-realtime-transport-lib/src/lib.rs"
required = (manifest_path, main_path, native_path, wasm_path, common_path)
if not all(path.exists() for path in required):
return violations
manifest = manifest_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
native = native_path.read_text(encoding="utf-8")
wasm = wasm_path.read_text(encoding="utf-8")
common = common_path.read_text(encoding="utf-8")
required_fragments = (
(manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-093: datagram smoke must consume the WebTransport backend directly"),
(main, 'max_datagram_size()', "DIST-LAYOUT-094: datagram smoke must inspect the negotiated backend payload bound"),
(main, 'send_datagram(', "DIST-LAYOUT-095: datagram smoke must send backend-specific datagrams"),
(main, 'receive_datagram()', "DIST-LAYOUT-096: datagram smoke must receive backend-specific datagrams"),
(main, 'game-realtime-webtransport-datagram-smoke: PASS', "DIST-LAYOUT-097: datagram smoke must expose a deterministic PASS verdict"),
(native, 'pub fn send_datagram(&self, payload: &[u8])', "DIST-LAYOUT-098: native WebTransport session must own the backend-specific datagram send capability"),
(wasm, 'pub async fn send_datagram(&self, payload: &[u8])', "DIST-LAYOUT-099: browser WebTransport session must expose the backend-specific datagram send capability"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
if "datagram" in common.lower():
violations.append("DIST-LAYOUT-100: transport-neutral realtime contract must remain free of datagram semantics")
for fragment in ("engine-v1-", "game-reflex-poc", "game-snake-poc"):
if fragment in manifest:
violations.append(f"DIST-LAYOUT-101: datagram smoke must not depend on engine/gameplay package: {fragment}")
return violations
def audit_android_native_gradle(root: pathlib.Path) -> list[str]:
"""Validate the minimum Gradle contract and multi-ABI native Rust consumers."""
violations: list[str] = []
settings_path = root / "Android/settings.gradle"
native_script_path = root / "Android/gradle/sasedev-rust-android.gradle"
snake_build_path = root / "Android/game-snake-poc/build.gradle"
reflex_build_path = root / "Android/game-reflex-poc/build.gradle"
required = (settings_path, native_script_path, snake_build_path, reflex_build_path)
if not all(path.exists() for path in required):
return violations
settings = settings_path.read_text(encoding="utf-8")
settings_fragments = (
'import org.gradle.util.GradleVersion',
'GradleVersion.version("9.6.0")',
'GradleVersion.current()',
'currentGradleVersion < minimumGradleVersion',
)
for fragment in settings_fragments:
if fragment not in settings:
violations.append(f"DIST-LAYOUT-072: Android settings missing minimum Gradle gate: {fragment}")
native_script = native_script_path.read_text(encoding="utf-8")
native_fragments = (
'spec.commandLine(cargoCommand)',
'"cargo",',
'"ndk",',
'cargoCommand.add("--release")',
'releaseBuild.set(selectedReleaseBuild)',
'onVariants(selector().all())',
'"--no-default-features",',
'variant.sources.jniLibs.addGeneratedSourceDirectory',
'generated/sasedevNative/',
'libgame_android_entrypoint.so',
'libSDL3.so',
)
for fragment in native_fragments:
if fragment not in native_script:
violations.append(f"DIST-LAYOUT-074: Gradle-owned Rust Android build missing contract: {fragment}")
if "build_android_rust.py" in native_script or "python" in native_script.lower():
violations.append("DIST-LAYOUT-075: Gradle-owned Rust Android build must not delegate orchestration to Python")
consumers = (
(
"Snake",
snake_build_path.read_text(encoding="utf-8"),
'ext.sasedevRustGameFeature = "snake"',
),
(
"Reflex",
reflex_build_path.read_text(encoding="utf-8"),
'ext.sasedevRustGameFeature = "reflex"',
),
)
shared_fragments = (
'ext.sasedevRustAndroidAbis = ["arm64-v8a", "armeabi-v7a", "x86_64", "x86"]',
'ext.sasedevRustAndroidApi = 21',
'minSdk 21',
"abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'",
'apply from: rootProject.file("gradle/sasedev-rust-android.gradle")',
)
for label, content, feature_fragment in consumers:
for fragment in (feature_fragment, *shared_fragments):
if fragment not in content:
violations.append(f"DIST-LAYOUT-076: {label} Android multi-ABI consumer missing contract: {fragment}")
return violations
def main() -> int:
"""Validate that every static distribution boundary exists."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".", help="workspace root")
arguments = parser.parse_args()
root = pathlib.Path(arguments.root).resolve()
missing: list[str] = []
forbidden_present: list[str] = []
contract_violations: list[str] = []
for relative in REQUIRED_PATHS:
if not (root / relative).exists():
missing.append(relative)
for relative in FORBIDDEN_PATHS:
if (root / relative).exists():
forbidden_present.append(relative)
contract_violations.extend(audit_snake_web_shell(root))
contract_violations.extend(audit_snake_web_integration(root))
contract_violations.extend(audit_snake_tauri_android_integration(root))
contract_violations.extend(audit_snake_tauri_generated_android(root))
contract_violations.extend(audit_android_native_gradle(root))
contract_violations.extend(audit_realtime_fallback_smoke(root))
contract_violations.extend(audit_realtime_transport_measure(root))
contract_violations.extend(audit_webtransport_browser_smoke(root))
contract_violations.extend(audit_webtransport_datagram_smoke(root))
if missing:
for relative in missing:
print(f"DIST-LAYOUT-001: missing required path: {relative}", file=sys.stderr)
print(f"Distribution layout audit: {len(missing)} violation(s)", file=sys.stderr)
return 1
if forbidden_present:
for relative in forbidden_present:
print(f"DIST-LAYOUT-002: generated path must stay outside repository: {relative}", file=sys.stderr)
print(f"Distribution layout audit: {len(forbidden_present)} violation(s)", file=sys.stderr)
return 1
if contract_violations:
for violation in contract_violations:
print(violation, file=sys.stderr)
print(f"Distribution layout audit: {len(contract_violations)} violation(s)", file=sys.stderr)
return 1
print(f"Distribution layout audit: clean ({len(REQUIRED_PATHS)} required path(s), {len(FORBIDDEN_PATHS)} forbidden path(s) absent)")
return 0
if __name__ == "__main__":
raise SystemExit(main())