247 lines
14 KiB
Python
247 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# file: scripts/audit_distribution_layout.py
|
|
# version: 10
|
|
|
|
"""Audit the static distribution layout expected by supported POCs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import pathlib
|
|
import sys
|
|
|
|
|
|
FORBIDDEN_PATHS = (
|
|
"builds",
|
|
)
|
|
|
|
|
|
REQUIRED_PATHS = (
|
|
"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/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",
|
|
"scripts/build_android_rust.py",
|
|
"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 minimal Snake Tauri Android host boundary."""
|
|
|
|
violations: list[str] = []
|
|
cargo_path = root / "crates/apps/game-snake-poc-tauri/Cargo.toml"
|
|
package_path = root / "crates/apps/game-snake-poc-tauri/package.json"
|
|
tauri_path = root / "crates/apps/game-snake-poc-tauri/tauri.conf.json"
|
|
tauri_rust_path = root / "crates/apps/game-snake-poc-tauri/src/tauri.rs"
|
|
build_script_path = root / "crates/apps/game-snake-poc-tauri/build.rs"
|
|
frontend_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/main.ts"
|
|
logging_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/logging.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, frontend_path, logging_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")
|
|
frontend = frontend_path.read_text(encoding="utf-8")
|
|
logging = logging_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"),
|
|
(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"),
|
|
(frontend, 'configure_runtime_provenance(', "DIST-LAYOUT-029: Snake Tauri frontend must configure provenance through the shared adapter"),
|
|
(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"),
|
|
(frontend, "console.", "DIST-LAYOUT-035: Snake Tauri application frontend must not log directly through console.*"),
|
|
)
|
|
for content, fragment, message in forbidden_fragments:
|
|
if fragment in content.lower():
|
|
violations.append(message)
|
|
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))
|
|
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())
|