soteria/scripts/structure_hygiene_check.py

90 lines
2.2 KiB
Python

#!/usr/bin/env python3
"""Enforce lightweight naming/layout hygiene for Soteria sources."""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
GENERIC_TOKENS = {
"tmp",
"temp",
"foo",
"bar",
"baz",
"misc",
"new",
"old",
"final",
"wip",
}
MAX_DEPTH = 10
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
return parser.parse_args()
def iter_source_files(root: Path) -> list[Path]:
files: list[Path] = []
for rel_root, suffixes in (
("cmd", {".go"}),
("internal", {".go"}),
("web/src", {".ts", ".tsx", ".css"}),
):
base = root / rel_root
if not base.exists():
continue
for path in sorted(base.rglob("*")):
if not path.is_file():
continue
if path.suffix not in suffixes:
continue
rel = path.relative_to(root).as_posix()
if rel.endswith("_test.go"):
continue
if rel.startswith("internal/server/ui-dist/"):
continue
files.append(path)
return files
def filename_tokens(path: Path) -> list[str]:
stem = path.stem
return [tok for tok in re.split(r"[-_]", stem) if tok]
def main() -> int:
args = parse_args()
root = Path(args.root).resolve()
violations: list[str] = []
files = iter_source_files(root)
for path in files:
rel = path.resolve().relative_to(root).as_posix()
depth = len(Path(rel).parts)
if depth > MAX_DEPTH:
violations.append(f"{rel}: depth {depth} exceeds {MAX_DEPTH}")
tokens = {tok.lower() for tok in filename_tokens(path)}
bad = sorted(tokens.intersection(GENERIC_TOKENS))
if bad:
violations.append(f"{rel}: non-descriptive filename token(s): {', '.join(bad)}")
if violations:
print("Structure hygiene check failed:")
for item in violations:
print(item)
return 1
print("Structure hygiene checks: ok")
return 0
if __name__ == "__main__":
sys.exit(main())