0.1.0-0-pre.1

This commit is contained in:
2026-09-15 23:26:37 +02:00
commit 8b4c79c431
57 changed files with 2314 additions and 0 deletions

251
scripts/audit_markdown_tables.py Executable file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# file: scripts/audit_markdown_tables.py
# version: 1
"""Validate games.sasedev Markdown tables and vertical spacing for explicitly supplied files or directories."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
_SEPARATOR_CELL = re.compile(r"^:?-{3,}:?$")
_IGNORED_DIRECTORY_NAMES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target"})
def _markdown_files(paths: list[str]) -> list[pathlib.Path]:
files: list[pathlib.Path] = []
for raw_path in paths:
path = pathlib.Path(raw_path)
if path.is_dir():
files.extend(
sorted(
candidate
for candidate in path.rglob("*.md")
if candidate.is_file() and not any(part in _IGNORED_DIRECTORY_NAMES for part in candidate.parts)
)
)
elif path.is_file() and path.suffix.lower() == ".md":
files.append(path)
else:
print(f"Markdown table audit: unsupported or missing path: {path}", file=sys.stderr)
return sorted(set(files))
def _is_table_row(line: str) -> bool:
return line.startswith("|") and line.endswith("|")
def _cells(line: str) -> list[str]:
return line.split("|")[1:-1]
def _is_separator_row(line: str) -> bool:
if not _is_table_row(line):
return False
cells = _cells(line)
return bool(cells) and all(_separator_marker(cell) is not None for cell in cells)
def _separator_marker(cell: str) -> str | None:
marker = cell.strip()
if _SEPARATOR_CELL.fullmatch(marker) is None:
return None
return marker
def _separator_alignment(marker: str) -> str:
if marker.startswith(":") and marker.endswith(":"):
return "center"
if marker.endswith(":"):
return "right"
return "left"
def _space_padding(cell: str) -> tuple[int, int]:
left = len(cell) - len(cell.lstrip(" "))
right = len(cell) - len(cell.rstrip(" "))
return left, right
def _validate_table(path: pathlib.Path, start_line: int, rows: list[str]) -> list[str]:
errors: list[str] = []
if any("\\|" in row for row in rows):
errors.append(f"{path}:{start_line}: escaped pipe is forbidden inside Markdown table cells")
header_cells = _cells(rows[0])
column_count = len(header_cells)
parsed_rows = [_cells(row) for row in rows]
for offset, row_cells in enumerate(parsed_rows):
if len(row_cells) != column_count:
errors.append(
f"{path}:{start_line + offset}: table row has {len(row_cells)} columns; expected {column_count}; "
"a literal pipe inside a cell is forbidden"
)
return errors
for column_index in range(column_count):
raw_cells = [row[column_index] for row in parsed_rows]
widths = [len(cell) for cell in raw_cells]
expected_width = widths[0]
if any(width != expected_width for width in widths):
errors.append(
f"{path}:{start_line}: column {column_index + 1} is not vertically aligned; raw widths are {widths}"
)
continue
content_cells = [cell for row_index, cell in enumerate(raw_cells) if row_index != 1]
if not content_cells:
errors.append(f"{path}:{start_line}: column {column_index + 1} has no header/data content")
continue
max_content_width = max(len(cell.strip()) for cell in content_cells)
marker = _separator_marker(raw_cells[1])
if marker is None:
errors.append(f"{path}:{start_line + 1}: separator for column {column_index + 1} is malformed")
continue
minimum_separator_width = 3 + marker.count(":")
required_width = max(max_content_width + 2, minimum_separator_width)
if expected_width != required_width:
errors.append(
f"{path}:{start_line}: column {column_index + 1} width is {expected_width}; expected {required_width} "
"(longest content plus outer padding, or the minimum Markdown separator width)"
)
alignment = _separator_alignment(marker)
for cell in content_cells:
content_width = len(cell.strip())
left_padding, right_padding = _space_padding(cell)
if content_width + left_padding + right_padding != len(cell) or left_padding < 1 or right_padding < 1:
errors.append(
f"{path}:{start_line}: column {column_index + 1} content cells must use spaces only for outer alignment padding"
)
break
expected_padding = expected_width - content_width
if alignment == "left" and (left_padding != 1 or right_padding != expected_padding - 1):
errors.append(
f"{path}:{start_line}: column {column_index + 1} is left-aligned; content must use one leading space and right padding only"
)
break
if alignment == "right" and (right_padding != 1 or left_padding != expected_padding - 1):
errors.append(
f"{path}:{start_line}: column {column_index + 1} is right-aligned; content must use left padding and one trailing space"
)
break
if alignment == "center" and abs(left_padding - right_padding) > 1:
errors.append(
f"{path}:{start_line}: column {column_index + 1} is centered; left and right padding may differ by at most one space"
)
break
separator = raw_cells[1]
if len(separator) != expected_width or _SEPARATOR_CELL.fullmatch(separator) is None:
errors.append(
f"{path}:{start_line + 1}: separator for column {column_index + 1} must fill the exact column width with hyphens and optional alignment colons"
)
return errors
def _validate_blank_lines(path: pathlib.Path, lines: list[str]) -> list[str]:
errors: list[str] = []
fence_marker: str | None = None
blank_run_start: int | None = None
blank_run_length = 0
def flush_blank_run() -> None:
nonlocal blank_run_start, blank_run_length
if blank_run_start is not None and blank_run_length >= 2:
errors.append(
f"{path}:{blank_run_start}: {blank_run_length} consecutive blank lines are forbidden outside fenced code blocks; keep at most one"
)
blank_run_start = None
blank_run_length = 0
for line_number, line in enumerate(lines, start=1):
stripped = line.lstrip()
if stripped.startswith("```") or stripped.startswith("~~~"):
flush_blank_run()
marker = stripped[:3]
if fence_marker is None:
fence_marker = marker
elif marker == fence_marker:
fence_marker = None
continue
if fence_marker is not None:
continue
if line.strip() == "":
if blank_run_start is None:
blank_run_start = line_number
blank_run_length += 1
continue
flush_blank_run()
flush_blank_run()
return errors
def _audit_file(path: pathlib.Path) -> tuple[int, list[str]]:
lines = path.read_text(encoding="utf-8").splitlines()
errors = _validate_blank_lines(path, lines)
table_count = 0
fence_marker: str | None = None
index = 0
while index < len(lines):
stripped = lines[index].lstrip()
if stripped.startswith("```") or stripped.startswith("~~~"):
marker = stripped[:3]
if fence_marker is None:
fence_marker = marker
elif marker == fence_marker:
fence_marker = None
index += 1
continue
if fence_marker is not None:
index += 1
continue
if index + 1 < len(lines) and _is_table_row(lines[index]) and _is_separator_row(lines[index + 1]):
start = index
rows = [lines[index], lines[index + 1]]
index += 2
while index < len(lines) and _is_table_row(lines[index]):
rows.append(lines[index])
index += 1
table_count += 1
errors.extend(_validate_table(path, start + 1, rows))
continue
index += 1
return table_count, errors
def main() -> int:
"""Audit games.sasedev Markdown formatting in the explicitly selected scope."""
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="+", help="Markdown files or directories to audit")
arguments = parser.parse_args()
files = _markdown_files(arguments.paths)
if not files:
print("Markdown table audit: no Markdown files selected", file=sys.stderr)
return 2
total_tables = 0
all_errors: list[str] = []
for path in files:
table_count, errors = _audit_file(path)
total_tables += table_count
all_errors.extend(errors)
if all_errors:
for error in all_errors:
print(error, file=sys.stderr)
print(f"Markdown table audit: {len(all_errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Markdown table audit: clean ({total_tables} table(s), {len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# file: scripts/audit_project_workspace_rules.py
# version: 2
"""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")
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()}")
for java_path in sorted(root.rglob("*.java")):
if not java_path.is_relative_to(root / "Android"):
errors.append(f"GAME-ANDROID-001: Java source outside Android/: {java_path.relative_to(root).as_posix()}")
if errors:
for error in errors:
print(error, file=sys.stderr)
print(f"games.sasedev workspace audit: {len(errors)} violation(s)", file=sys.stderr)
return 1
print("games.sasedev workspace audit: clean")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_export_completeness.py
# version: 1
"""Audit crate-root export completeness and canonical same-crate paths."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
from audit_rust_general_rules import line_depths, mask_rust_source
@dataclasses.dataclass(frozen=True)
class Candidate:
"""One export/path normalization candidate."""
code: str
path: str
line: int
message: str
@dataclasses.dataclass(frozen=True)
class Declaration:
"""One module-level public or crate-public declaration."""
module: str
name: str
visibility: str
kind: str
path: pathlib.Path
line: int
def crate_roots(root: pathlib.Path) -> list[pathlib.Path]:
"""Return Rust workspace crate directories under crates/."""
crates: list[pathlib.Path] = []
for manifest in (root / "crates").glob("*/Cargo.toml"):
crate = manifest.parent
if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file():
crates.append(crate)
return sorted(crates)
def module_path(crate: pathlib.Path, path: pathlib.Path) -> str:
"""Return the Rust module path represented by a source file."""
relative = path.relative_to(crate / "src").with_suffix("")
return "::".join(relative.parts)
def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]:
"""Return module-level public declarations from one source module."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
depths = line_depths(mask_rust_source(text))
found: list[Declaration] = []
pattern = re.compile(
r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)"
)
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = pattern.match(line)
if match is not None:
found.append(Declaration(module_path(crate, path), match.group(3), match.group(1), match.group(2), path, idx))
return found
def private_declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]:
"""Return module-level strictly private declarations from one source module."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
depths = line_depths(mask_rust_source(text))
found: list[Declaration] = []
pattern = re.compile(r"^\s*(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)")
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
if line.lstrip().startswith(("pub ", "pub(crate) ")):
continue
match = pattern.match(line)
if match is not None:
found.append(Declaration(module_path(crate, path), match.group(2), "private", match.group(1), path, idx))
return found
def unqualified_test_reference_pattern(declaration: Declaration) -> re.Pattern[str]:
"""Return a conservative pattern for one unqualified parent-item reference in a separated unit test."""
name = re.escape(declaration.name)
if declaration.kind == "fn":
return re.compile(rf"(?<![\w:.]){name}\s*\(")
if declaration.kind in {"const", "static", "type", "struct", "enum", "trait", "union"}:
return re.compile(rf"(?<![\w:]){name}\b")
return re.compile(rf"(?<![\w:]){name}\b")
def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
"""Return explicit crate-root re-exports keyed by source module and symbol."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
exports: dict[tuple[str, str], str] = {}
if not crate_root.is_file():
return exports
pattern = re.compile(
r"^pub(?:\(crate\))?\s+use\s+self::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)\s*;",
re.MULTILINE,
)
for match in pattern.finditer(crate_root.read_text(encoding="utf-8")):
exports[(match.group(1), match.group(2))] = match.group(2)
return exports
def unit_test_parents(crate: pathlib.Path) -> dict[pathlib.Path, pathlib.Path]:
"""Map separated unit-test files to the production module that owns them."""
mapping: dict[pathlib.Path, pathlib.Path] = {}
pattern = re.compile(r'#\[path\s*=\s*"\.\./unit_tests/([^\"]+)"\]')
for source in sorted((crate / "src").rglob("*.rs")):
for match in pattern.finditer(source.read_text(encoding="utf-8")):
test = crate / "unit_tests" / match.group(1)
if test.is_file():
mapping[test.resolve()] = source.resolve()
return mapping
def external_usage(crate: pathlib.Path, declaration: Declaration, test_parents: dict[pathlib.Path, pathlib.Path]) -> bool:
"""Return whether a crate-public item is referenced outside its declaration module."""
direct = f"crate::{declaration.module}::{declaration.name}"
root_direct = f"crate::{declaration.name}"
super_ref = f"super::{declaration.name}"
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts:
continue
resolved = path.resolve()
if resolved == declaration.path.resolve():
continue
text = path.read_text(encoding="utf-8")
if direct in text or root_direct in text:
return True
if test_parents.get(resolved) == declaration.path.resolve() and super_ref in text:
return True
return False
def crate_root_symbols(crate: pathlib.Path) -> set[str]:
"""Return names that can resolve directly after `crate::`."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
symbols: set[str] = set()
if crate_root.is_file():
text = crate_root.read_text(encoding="utf-8")
for match in re.finditer(r"^\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*(?:(?:pub|pub\(crate\))\s+)?(?:(?:async|const|unsafe)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*pub(?:\(crate\))?\s+use\s+[^;]*::([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE):
symbols.add(match.group(1))
for match in re.finditer(r"^\s*pub\s+extern\s+crate\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", text, re.MULTILINE):
symbols.add(match.group(1))
for path in sorted((crate / "src").rglob("*.rs")):
text = path.read_text(encoding="utf-8")
pattern = re.compile(r"#\[macro_export\]\s*\n\s*macro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)")
for match in pattern.finditer(text):
symbols.add(match.group(1))
return symbols
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
"""Audit one crate for export completeness and canonical paths."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
exports = root_exports(crate)
tests = unit_test_parents(crate)
declarations: dict[tuple[str, str], Declaration] = {}
private_declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
candidates: list[Candidate] = []
for path in sorted((crate / "src").rglob("*.rs")):
if path == crate_root:
continue
private_declarations_by_source[path.resolve()] = {item.name: item for item in private_declaration_candidates(crate, path)}
for declaration in declaration_candidates(crate, path):
key = (declaration.module, declaration.name)
declarations[key] = declaration
required = declaration.visibility == "pub" or external_usage(crate, declaration, tests)
if required and key not in exports:
relative = path.relative_to(workspace).as_posix()
reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module"
candidates.append(Candidate("RUST-API-201", relative, declaration.line, f"{reason} `{declaration.name}` requires a crate-root re-export"))
# Canonical crate::Item paths for exported items.
long_path = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)")
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts or path == crate_root:
continue
relative = path.relative_to(workspace).as_posix()
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
for match in long_path.finditer(line):
alias = exports.get((match.group(1), match.group(2)))
if alias is not None:
candidates.append(Candidate("RUST-IMPORT-201", relative, idx, f"long internal path `crate::{match.group(1)}::{match.group(2)}` must use `crate::{alias}`"))
# Simple `crate::Item` references must resolve at the crate root.
root_symbols = crate_root_symbols(crate)
simple_root = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_]*)\b(?!::)")
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts:
continue
relative = path.relative_to(workspace).as_posix()
text = path.read_text(encoding="utf-8")
masked = mask_rust_source(text)
for idx, line in enumerate(masked.splitlines(), 1):
for match in simple_root.finditer(line):
symbol = match.group(1)
if symbol not in root_symbols:
candidates.append(Candidate("RUST-IMPORT-203", relative, idx, f"`crate::{symbol}` does not resolve to a declared/re-exported crate-root symbol"))
# Separated unit tests use `super::Item` only for strictly private parent items; visible items use the crate-root façade.
declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
for declaration in declarations.values():
declarations_by_source.setdefault(declaration.path.resolve(), {})[declaration.name] = declaration
super_pattern = re.compile(r"\bsuper::([A-Za-z_][A-Za-z0-9_]*)")
for test, parent in tests.items():
parent_declarations = declarations_by_source.get(parent, {})
parent_private_declarations = private_declarations_by_source.get(parent, {})
relative = test.relative_to(workspace).as_posix()
text = test.read_text(encoding="utf-8")
masked_lines = mask_rust_source(text).splitlines()
for idx, line in enumerate(masked_lines, 1):
for match in super_pattern.finditer(line):
declaration = parent_declarations.get(match.group(1))
if declaration is not None and declaration.visibility in {"pub", "pub(crate)"}:
candidates.append(Candidate("RUST-IMPORT-202", relative, idx, f"`super::{declaration.name}` targets {declaration.visibility}; use crate-root `crate::{declaration.name}`"))
for declaration in parent_private_declarations.values():
if unqualified_test_reference_pattern(declaration).search(line) is not None:
candidates.append(Candidate("RUST-IMPORT-204", relative, idx, f"strictly private parent item `{declaration.name}` must be accessed as `super::{declaration.name}` in separated unit tests"))
for declaration in parent_declarations.values():
if unqualified_test_reference_pattern(declaration).search(line) is not None:
candidates.append(Candidate("RUST-IMPORT-205", relative, idx, f"{declaration.visibility} parent item `{declaration.name}` must be accessed through crate-root `crate::{declaration.name}` in separated unit tests"))
return candidates
def main() -> int:
"""Run the export-completeness audit."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
parser.add_argument("--summary-only", action="store_true")
arguments = parser.parse_args()
workspace = pathlib.Path(arguments.root).resolve()
candidates = [item for crate in crate_roots(workspace) for item in audit_crate(workspace, crate)]
candidates.sort(key=lambda item: (item.code, item.path, item.line, item.message))
counts: dict[str, int] = {}
for item in candidates:
counts[item.code] = counts.get(item.code, 0) + 1
sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s)\n")
for code in sorted(counts):
sys.stdout.write(f"{code}: {counts[code]}\n")
if not arguments.summary_only:
for item in candidates:
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
return 0 if arguments.report_only or not candidates else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,587 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_general_rules.py
# version: 1
"""Audit mechanically verifiable Rust normalization rules used by games.sasedev."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
@dataclasses.dataclass(frozen=True)
class Violation:
"""One mechanically detected general Rust rule violation."""
code: str
path: str
line: int
message: str
def rust_files(root: pathlib.Path) -> list[pathlib.Path]:
"""Return tracked-style Rust sources outside generated directories."""
crates = root / "crates"
return sorted(path for path in crates.rglob("*.rs") if "target" not in path.parts and ".git" not in path.parts)
def mask_rust_source(text: str) -> str:
"""Mask comments and literals while preserving braces, newlines and offsets."""
output = list(text)
index = 0
state = "code"
block_depth = 0
raw_hashes = 0
while index < len(text):
char = text[index]
nxt = text[index + 1] if index + 1 < len(text) else ""
if state == "code":
if char == "/" and nxt == "/":
output[index] = " "
output[index + 1] = " "
index += 2
state = "line_comment"
continue
if char == "/" and nxt == "*":
output[index] = " "
output[index + 1] = " "
index += 2
state = "block_comment"
block_depth = 1
continue
if char == '"':
output[index] = " "
index += 1
state = "string"
continue
if char == "'":
# Treat a quote as a char literal only when it can close shortly;
# lifetimes such as `'a` remain code.
probe = index + 1
escaped = False
found = False
while probe < min(len(text), index + 8) and text[probe] != "\n":
if not escaped and text[probe] == "'":
found = True
break
if not escaped and text[probe] == "\\":
escaped = True
else:
escaped = False
probe += 1
if found:
output[index] = " "
index += 1
state = "char"
continue
if char == "r":
probe = index + 1
hashes = 0
while probe < len(text) and text[probe] == "#":
hashes += 1
probe += 1
if probe < len(text) and text[probe] == '"':
for masked in range(index, probe + 1):
output[masked] = " "
index = probe + 1
raw_hashes = hashes
state = "raw_string"
continue
index += 1
continue
if state == "line_comment":
if char == "\n":
state = "code"
else:
output[index] = " "
index += 1
continue
if state == "block_comment":
if char == "/" and nxt == "*":
output[index] = " "
output[index + 1] = " "
block_depth += 1
index += 2
continue
if char == "*" and nxt == "/":
output[index] = " "
output[index + 1] = " "
block_depth -= 1
index += 2
if block_depth == 0:
state = "code"
continue
if char != "\n":
output[index] = " "
index += 1
continue
if state == "string":
if char == "\\" and index + 1 < len(text):
output[index] = " "
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == '"':
output[index] = " "
state = "code"
elif char != "\n":
output[index] = " "
index += 1
continue
if state == "char":
if char == "\\" and index + 1 < len(text):
output[index] = " "
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == "'":
output[index] = " "
state = "code"
elif char != "\n":
output[index] = " "
index += 1
continue
if state == "raw_string":
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
output[index] = " "
for masked in range(index + 1, index + 1 + raw_hashes):
output[masked] = " "
index += 1 + raw_hashes
state = "code"
continue
if char != "\n":
output[index] = " "
index += 1
continue
return "".join(output)
def line_depths(masked: str) -> list[int]:
"""Return brace depth at the start of every line."""
depths: list[int] = []
depth = 0
for line in masked.splitlines():
depths.append(depth)
depth += line.count("{") - line.count("}")
if depth < 0:
depth = 0
return depths
def preceding_doc_line(lines: list[str], index: int) -> bool:
"""Return whether one declaration has adjacent useful line rustdoc."""
cursor = index - 2
while cursor >= 0 and lines[cursor].strip().startswith("#["):
cursor -= 1
return cursor >= 0 and lines[cursor].lstrip().startswith("///")
def natural_key(value: str) -> tuple[tuple[int, object], ...]:
"""Return a natural case-sensitive ordering key."""
tokens = re.findall(r"[A-Za-z_]+|[0-9]+", value)
return tuple((1, int(token)) if token.isdigit() else (0, token) for token in tokens)
def public_declaration(stripped: str) -> re.Match[str] | None:
"""Match a public or crate-public item or associated item declaration."""
return re.match(
r"^pub(?:\(crate\))?\s+(?:(?:async|const|unsafe)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)",
stripped,
)
def public_field(stripped: str) -> re.Match[str] | None:
"""Match a public or crate-public named field declaration."""
return re.match(r"^pub(?:\(crate\))?\s+([A-Za-z_][A-Za-z0-9_]*)\s*:", stripped)
def declaration_start(stripped: str, kind: str) -> bool:
"""Return whether a line starts a declaration of one requested kind."""
prefixes = r"(?:(?:pub|pub\(crate\))\s+)?(?:(?:async|const|unsafe)\s+)*"
return re.match(rf"^{prefixes}{kind}\b", stripped) is not None
@dataclasses.dataclass(frozen=True)
class RustItem:
"""One mechanically detected Rust item used for spacing checks."""
kind: str
visibility: str
start: int
declaration: int
end: int
depth: int
braced: bool
def item_visibility(stripped: str) -> str:
"""Return normalized visibility for one item declaration."""
if stripped.startswith("pub(crate) "):
return "pub(crate)"
if stripped.startswith("pub "):
return "pub"
return "private"
def item_kind(stripped: str) -> str | None:
"""Return the normalized declaration kind for spacing checks."""
prefix = r"(?:(?:pub|pub\(crate\))\s+)?"
qualifiers = r"(?:(?:async|const|unsafe)\s+)*"
if re.match(rf"^{prefix}{qualifiers}fn\b", stripped):
return "fn"
for kind in ("struct", "enum", "union", "trait", "impl", "const", "static", "type"):
if re.match(rf"^{prefix}{qualifiers}{kind}\b", stripped):
return kind
return None
def item_leading_line(lines: list[str], declaration_index: int) -> int:
"""Return the first rustdoc/attribute line attached to an item."""
cursor = declaration_index - 2
while cursor >= 0:
stripped = lines[cursor].strip()
if stripped.startswith("///") or stripped.startswith("#["):
cursor -= 1
continue
break
return cursor + 2
def item_end_line(masked_lines: list[str], depths: list[int], declaration_index: int, kind: str) -> tuple[int, bool]:
"""Return the end line and whether the item owns a braced body."""
start_depth = depths[declaration_index - 1]
if kind in {"const", "static", "type"}:
for line_index in range(declaration_index - 1, len(masked_lines)):
if ";" in masked_lines[line_index]:
return line_index + 1, False
return declaration_index, False
body_started = False
for line_index in range(declaration_index - 1, len(masked_lines)):
masked_line = masked_lines[line_index]
if not body_started and ";" in masked_line and "{" not in masked_line:
return line_index + 1, False
if "{" in masked_line:
body_started = True
if body_started:
end_depth = depths[line_index] + masked_line.count("{") - masked_line.count("}")
if end_depth == start_depth:
return line_index + 1, True
return declaration_index, body_started
def rust_items(lines: list[str], masked_lines: list[str], depths: list[int]) -> list[RustItem]:
"""Return mechanically detected Rust items with source spans."""
result: list[RustItem] = []
for idx, line in enumerate(lines, 1):
stripped = line.strip()
kind = item_kind(stripped)
if kind is None:
continue
end, braced = item_end_line(masked_lines, depths, idx, kind)
result.append(RustItem(kind, item_visibility(stripped), item_leading_line(lines, idx), idx, end, depths[idx - 1], braced))
return result
def item_parent(item: RustItem, items: list[RustItem]) -> RustItem | None:
"""Return the smallest braced item that contains another item."""
parents = [candidate for candidate in items if candidate.braced and candidate.declaration < item.declaration <= candidate.end and candidate.depth < item.depth]
if not parents:
return None
return max(parents, key=lambda candidate: candidate.depth)
def audit_item_spacing_and_nesting(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]:
"""Enforce exact item separation and reject declarations nested in functions."""
violations: list[Violation] = []
items = rust_items(lines, masked_lines, depths)
for item in items:
parent = item_parent(item, items)
if parent is not None and parent.kind == "fn":
body_depth = parent.depth + 1
preceding = lines[parent.declaration:item.declaration - 1]
preceding_depths = depths[parent.declaration:item.declaration - 1]
returned = any(depth == body_depth and re.match(r"^\s*return\b.*;\s*$", line) is not None for line, depth in zip(preceding, preceding_depths, strict=True))
if returned:
violations.append(Violation("RUST-FMT-110", relative, item.declaration, f"item `{item.kind}` follows an unconditional function-level return; probable misplaced closing brace"))
children: dict[tuple[int, int] | None, list[RustItem]] = {}
for item in items:
parent = item_parent(item, items)
key = None if parent is None else (parent.declaration, parent.end)
children.setdefault(key, []).append(item)
for siblings in children.values():
siblings.sort(key=lambda item: item.declaration)
for previous, current in zip(siblings, siblings[1:]):
# Do not infer spacing across unparsed syntax such as macro invocations.
between = lines[previous.end:current.start - 1]
if any(candidate.strip() for candidate in between):
continue
blank_count = sum(1 for candidate in between if not candidate.strip())
same_homogeneous_block = previous.kind == current.kind and previous.kind in {"const", "static", "type"} and previous.visibility == current.visibility
expected = 0 if same_homogeneous_block else 1
if blank_count != expected:
rule = "RUST-FMT-111" if expected == 1 else "RUST-FMT-112"
expectation = "exactly one blank line between Rust items" if expected == 1 else "no blank line inside one homogeneous declaration block"
violations.append(Violation(rule, relative, current.start, f"{expectation}; found {blank_count}"))
return violations
def audit_blank_lines_in_bodies(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]:
"""Reject blank lines inside functions/methods and struct/enum bodies."""
violations: list[Violation] = []
pending_kind: str | None = None
pending_depth = 0
body_kind: str | None = None
body_depth = 0
for idx, (line, masked_line) in enumerate(zip(lines, masked_lines, strict=True), 1):
stripped = line.strip()
if body_kind is not None:
if not stripped and depths[idx - 1] >= body_depth:
violations.append(Violation("RUST-FMT-101", relative, idx, f"blank line inside {body_kind} body"))
if depths[idx - 1] < body_depth:
body_kind = None
if body_kind is None and pending_kind is None:
if declaration_start(stripped, "fn"):
pending_kind = "function/method"
pending_depth = depths[idx - 1]
elif declaration_start(stripped, "struct"):
pending_kind = "struct"
pending_depth = depths[idx - 1]
elif declaration_start(stripped, "enum"):
pending_kind = "enum"
pending_depth = depths[idx - 1]
if pending_kind is not None and "{" in masked_line:
body_kind = pending_kind
body_depth = pending_depth + 1
pending_kind = None
elif pending_kind is not None and ";" in masked_line:
pending_kind = None
return violations
def audit_top_level_const_blocks(relative: str, lines: list[str], depths: list[int]) -> list[Violation]:
"""Audit visibility/order/spacing for mechanically homogeneous const blocks."""
violations: list[Violation] = []
const_pattern = re.compile(r"^(pub\s+|pub\(crate\)\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)")
previous: tuple[int, int, str] | None = None
# rank: public, crate-public, private
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = const_pattern.match(line.strip())
if match is None:
# Docs and attributes belong to the surrounding block and do not break it.
if line.strip() and not line.lstrip().startswith(("///", "#[")):
previous = None
continue
visibility = match.group(1) or ""
rank = 0 if visibility == "pub " else 1 if visibility == "pub(crate) " else 2
name = match.group(2)
if previous is not None:
previous_line, previous_rank, previous_name = previous
if rank < previous_rank:
violations.append(Violation("RUST-FMT-103", relative, idx, "const visibility order must be pub, pub(crate), then private"))
if rank == previous_rank and natural_key(name) < natural_key(previous_name):
violations.append(Violation("RUST-FMT-104", relative, idx, "const block is not alphabetically ordered"))
previous = (idx, rank, name)
return violations
def audit_module_order(relative: str, lines: list[str], depths: list[int]) -> list[Violation]:
"""Audit top-level module declaration ordering with test modules last."""
violations: list[Violation] = []
modules: list[tuple[int, bool, str]] = []
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = re.match(r"^mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", line.strip())
if match is None:
continue
cursor = idx - 2
is_test = False
while cursor >= 0 and lines[cursor].strip().startswith("#["):
if "cfg(test)" in lines[cursor].replace(" ", ""):
is_test = True
cursor -= 1
modules.append((idx, is_test, match.group(1)))
production = [(idx, name) for idx, is_test, name in modules if not is_test]
tests = [(idx, name) for idx, is_test, name in modules if is_test]
if tests and production and min(idx for idx, _ in tests) < max(idx for idx, _ in production):
first_test = min(idx for idx, _ in tests)
violations.append(Violation("RUST-FMT-108", relative, first_test, "test modules must follow production modules"))
for group, label in ((production, "production"), (tests, "test")):
names = [name for _, name in group]
if names != sorted(names, key=natural_key):
for idx, name in group:
expected = sorted(names, key=natural_key)
if names.index(name) != expected.index(name):
violations.append(Violation("RUST-FMT-109", relative, idx, f"{label} module declarations are not alphabetically ordered"))
break
return violations
def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]:
"""Audit one Rust source against general normalization rules."""
relative = path.relative_to(root).as_posix()
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked = mask_rust_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
violations: list[Violation] = []
expected_header = f"// file: {relative}"
if not lines or lines[0] != expected_header:
violations.append(Violation("RUST-BASE-101", relative, 1, f"expected `{expected_header}`"))
if len(lines) < 2 or re.fullmatch(r"// version: [1-9][0-9]*", lines[1]) is None:
violations.append(Violation("RUST-BASE-102", relative, 2, "missing positive file version"))
if not text.endswith("\n") or text.endswith("\n\n"):
violations.append(Violation("RUST-FMT-100", relative, max(len(lines), 1), "file must end with exactly one newline"))
local_modules = {match.group(1) for line in lines if (match := re.fullmatch(r"\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", line)) is not None}
seen_declaration_before_use = False
use_rows: list[tuple[int, str]] = []
for idx, line in enumerate(lines, 1):
stripped = line.strip()
depth = depths[idx - 1] if idx - 1 < len(depths) else 0
if re.match(r"^(?:pub\s+)?extern\s+crate\s+[A-Za-z_][A-Za-z0-9_]*\s+as\s+", stripped):
violations.append(Violation("RUST-IMPORT-104", relative, idx, "extern crate aliases are forbidden"))
if "pub(in " in stripped or "pub(super)" in stripped:
violations.append(Violation("RUST-API-102", relative, idx, "restricted visibility is forbidden; use private or pub(crate) with crate-root re-export"))
if stripped.startswith("pub mod "):
violations.append(Violation("RUST-API-101", relative, idx, "modules must remain private and APIs use explicit re-exports"))
if depth == 0 and re.match(r"^(?:pub(?:\(crate\))?\s+)?(?:const|static|type|struct|enum|trait|union|fn|impl)\b", stripped):
seen_declaration_before_use = True
use_match = re.match(r"^(pub(?:\(crate\))?\s+)?use\s+", stripped)
if use_match is not None:
is_export = stripped.startswith("pub use ") or stripped.startswith("pub(crate) use ")
if depth != 0:
violations.append(Violation("RUST-IMPORT-111", relative, idx, "use declarations must be at module scope, never inside a function/method/block"))
if not is_export:
use_rows.append((idx, stripped))
if seen_declaration_before_use:
violations.append(Violation("RUST-IMPORT-112", relative, idx, "trait imports must remain at the beginning of the module before declarations"))
if "rust-rules: trait-import" not in stripped:
violations.append(Violation("RUST-IMPORT-101", relative, idx, "ordinary use requires an explicit trait import justification"))
if "::*" in stripped:
violations.append(Violation("RUST-IMPORT-102", relative, idx, "glob imports are forbidden"))
if "{" in stripped or "}" in stripped:
violations.append(Violation("RUST-IMPORT-103", relative, idx, "grouped use/re-export declarations are forbidden"))
if re.search(r"\s+as\s+[A-Za-z_][A-Za-z0-9_]*", stripped):
violations.append(Violation("RUST-IMPORT-104", relative, idx, "use/re-export aliases are forbidden"))
if is_export and re.match(r"^pub(?:\(crate\))?\s+use\s+crate::", stripped):
violations.append(Violation("RUST-IMPORT-105", relative, idx, "internal re-export must start with self::"))
declaration = public_declaration(stripped)
field = public_field(stripped)
if declaration is not None or field is not None:
if not preceding_doc_line(lines, idx):
name = (declaration or field).group(1)
violations.append(Violation("RUST-DOC-101", relative, idx, f"public/crate-public `{name}` requires adjacent rustdoc"))
# The ordinary module import block is contiguous and alphabetic.
for (previous_idx, previous_line), (current_idx, current_line) in zip(use_rows, use_rows[1:]):
between = lines[previous_idx:current_idx - 1]
if any(not candidate.strip() for candidate in between):
violations.append(Violation("RUST-IMPORT-113", relative, current_idx, "module use block contains an empty line"))
previous_path = previous_line.split("use ", 1)[1].split(";", 1)[0]
current_path = current_line.split("use ", 1)[1].split(";", 1)[0]
if natural_key(current_path) < natural_key(previous_path):
violations.append(Violation("RUST-IMPORT-114", relative, current_idx, "module use block is not alphabetically ordered"))
# Crate façade constraints.
if path.name in {"lib.rs", "main.rs"}:
for attribute in ("#![warn(missing_docs)]", "#![deny(unreachable_pub)]", "#![forbid(unsafe_code)]"):
if attribute not in lines[:24]:
violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`"))
exports: list[tuple[int, str]] = []
for idx, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("pub use ") or stripped.startswith("pub(crate) use "):
visibility = "pub(crate)" if stripped.startswith("pub(crate) use ") else "pub"
exports.append((idx, visibility))
if not preceding_doc_line(lines, idx):
violations.append(Violation("RUST-DOC-102", relative, idx, "crate-root re-export requires adjacent rustdoc"))
source_match = re.match(r"^pub(?:\(crate\))?\s+use\s+(?:self::)?([A-Za-z_][A-Za-z0-9_]*)::", stripped)
if source_match is not None and source_match.group(1) in local_modules and not re.match(r"^pub(?:\(crate\))?\s+use\s+self::", stripped):
violations.append(Violation("RUST-IMPORT-106", relative, idx, "crate-root internal re-export must start with self::"))
public_exports = [item for item in exports if item[1] == "pub"]
crate_exports = [item for item in exports if item[1] == "pub(crate)"]
if public_exports and crate_exports:
public_end = public_exports[-1][0]
crate_start = crate_exports[0][0]
transition = lines[public_end:crate_start - 1]
blank_count = sum(1 for candidate in transition if not candidate.strip())
if blank_count != 1:
violations.append(Violation("RUST-FMT-113", relative, crate_start, f"pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}"))
crate_seen = False
previous_by_visibility: dict[str, int] = {}
for idx, visibility in exports:
if visibility == "pub(crate)":
crate_seen = True
elif crate_seen:
violations.append(Violation("RUST-FMT-105", relative, idx, "pub use block must precede pub(crate) use block"))
previous_idx = previous_by_visibility.get(visibility)
if previous_idx is not None:
# One homogeneous block may contain rustdocs but no blank line.
# rustfmt is authoritative for intra-block re-export ordering;
# this audit must not impose a competing exported-symbol sort.
between = lines[previous_idx:idx - 1]
if any(not candidate.strip() for candidate in between):
violations.append(Violation("RUST-FMT-106", relative, idx, f"{visibility} use block contains an empty line"))
previous_by_visibility[visibility] = idx
violations.extend(audit_blank_lines_in_bodies(relative, lines, masked_lines, depths))
violations.extend(audit_item_spacing_and_nesting(relative, lines, masked_lines, depths))
violations.extend(audit_module_order(relative, lines, depths))
violations.extend(audit_top_level_const_blocks(relative, lines, depths))
return violations
def main() -> int:
"""Run the general Rust normalization audit."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
arguments = parser.parse_args()
root = pathlib.Path(arguments.root).resolve()
violations = [item for path in rust_files(root) for item in audit_file(root, path)]
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
if not violations:
sys.stdout.write("General Rust rule audit: clean\n")
return 0
sys.stdout.write(f"General Rust rule audit: {len(violations)} violation(s)\n")
counts: dict[str, int] = {}
for item in violations:
counts[item.code] = counts.get(item.code, 0) + 1
for code in sorted(counts):
sys.stdout.write(f"{code}: {counts[code]}\n")
for item in violations:
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
return 0 if arguments.report_only else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_workspace_rules.py
# version: 1
"""Run all Rust normalization and games.sasedev workspace audits."""
from __future__ import annotations
import argparse
import pathlib
import subprocess
import sys
def main() -> int:
"""Run general, export-completeness and project-specific audits."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".", help="workspace root")
arguments = parser.parse_args()
script_dir = pathlib.Path(__file__).resolve().parent
commands = [
["python3", str(script_dir / "audit_rust_general_rules.py"), "--root", arguments.root],
["python3", str(script_dir / "audit_rust_export_completeness.py"), "--root", arguments.root],
["python3", str(script_dir / "audit_project_workspace_rules.py"), "--root", arguments.root],
]
for command in commands:
completed = subprocess.run(command, check=False)
if completed.returncode != 0:
return completed.returncode
return 0
if __name__ == "__main__":
raise SystemExit(main())