#!/usr/bin/env python3 # file: scripts/audit_project_workspace_rules.py # version: 4 """Audit mechanically verifiable games.sasedev workspace boundaries.""" from __future__ import annotations import argparse import pathlib import re import sys import tomllib SEMVER = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0-pre|1-alpha|2-beta|3-rc)\.[1-9][0-9]*(?:\.fix\.[1-9][0-9]*)?)?$", re.ASCII) def main() -> int: """Run project-specific workspace audits.""" parser = argparse.ArgumentParser() parser.add_argument("--root", default=".", help="workspace root") arguments = parser.parse_args() root = pathlib.Path(arguments.root).resolve() errors: list[str] = [] manifest = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8")) workspace_version = manifest.get("workspace", {}).get("package", {}).get("version") if not isinstance(workspace_version, str) or SEMVER.fullmatch(workspace_version) is None: errors.append("VERSION-001: workspace.package.version does not follow the canonical games.sasedev SemVer scheme") members = manifest.get("workspace", {}).get("members", []) for member in members: if not isinstance(member, str) or not member.startswith("crates/"): errors.append(f"GAME-WS-002: workspace member must be under crates/: {member!r}") for manifest_path in sorted((root / "crates").rglob("Cargo.toml")): relative = manifest_path.relative_to(root).as_posix() data = tomllib.loads(manifest_path.read_text(encoding="utf-8")) package = data.get("package", {}) version = package.get("version") if isinstance(version, dict): if version.get("workspace") is not True: errors.append(f"GAME-WS-004: {relative}: table version must use workspace = true") elif isinstance(version, str): if SEMVER.fullmatch(version) is None: 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()}") history_root = root / "history" if history_root.exists(): for history_path in sorted(history_root.rglob("*.md")): relative_history = history_path.relative_to(history_root) if relative_history.as_posix() == "README.md": continue if len(relative_history.parts) != 2: errors.append(f"DOC-010: history file must use history//