Files
stundenfuchs/tools/policy_checks.py
T
maddin 6fbd1bb3c2
CI / checks (push) Has been cancelled
chore: initialize public repository
2026-03-22 12:57:09 +00:00

125 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATES_DIR = ROOT / "app" / "templates"
PAGES_DIR = TEMPLATES_DIR / "pages"
BASE_FILE = TEMPLATES_DIR / "base.html"
CSS_DIR = ROOT / "app" / "static" / "css"
RULE_EXTENDS = "POL001"
RULE_INLINE_STYLE = "POL002"
RULE_EXTRA_ASSETS = "POL003"
RULE_HEX_OUTSIDE_TOKENS = "POL004"
RULE_PX_SPACING = "POL005"
RULE_BASE_ASSETS = "POL006"
EXTENDS_BASE_RE = re.compile(r"\{%-?\s*extends\s+\"base\.html\"\s*-?%\}")
INLINE_STYLE_RE = re.compile(r"<style\b|style\s*=\s*\"", re.IGNORECASE)
ASSET_LINK_RE = re.compile(r"<link[^>]+rel=\"stylesheet\"|<script[^>]+src=\"", re.IGNORECASE)
HEX_RE = re.compile(r"#[0-9a-fA-F]{3,8}")
PX_SPACING_RE = re.compile(
r"(?:margin|padding|gap|row-gap|column-gap)\s*:\s*[^;]*\d+px",
flags=re.IGNORECASE,
)
def err(errors: list[str], path: Path, line_no: int, rule: str, message: str) -> None:
rel = path.relative_to(ROOT)
errors.append(f"{rel}:{line_no}: {rule} {message}")
def check_base_assets(errors: list[str]) -> None:
if not BASE_FILE.exists():
errors.append(f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} Missing base.html")
return
base_content = BASE_FILE.read_text(encoding="utf-8")
css_hits = re.findall(r'/static/css/[^\"]+', base_content)
js_hits = re.findall(r'/static/js/[^\"]+', base_content)
expected_css = ["/static/css/app.css?v={{ asset_version }}"]
expected_js = ["/static/js/app.js?v={{ asset_version }}"]
if css_hits != expected_css:
errors.append(
f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} expected CSS include {expected_css}, found {css_hits}"
)
if js_hits != expected_js:
errors.append(
f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} expected JS include {expected_js}, found {js_hits}"
)
def check_pages_extend_base(errors: list[str]) -> None:
page_files = sorted(PAGES_DIR.glob("*.html"))
if not page_files:
errors.append(f"{PAGES_DIR.relative_to(ROOT)}:1: {RULE_EXTENDS} No page templates found")
return
for path in page_files:
content = path.read_text(encoding="utf-8")
if not EXTENDS_BASE_RE.search(content):
err(
errors,
path,
1,
RULE_EXTENDS,
"page template must contain {% extends \"base.html\" %}",
)
def check_templates_inline_and_assets(errors: list[str]) -> None:
for path in sorted(TEMPLATES_DIR.rglob("*.html")):
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if INLINE_STYLE_RE.search(line):
err(errors, path, idx, RULE_INLINE_STYLE, "inline style or <style> is forbidden")
if path != BASE_FILE and ASSET_LINK_RE.search(line):
err(
errors,
path,
idx,
RULE_EXTRA_ASSETS,
"asset includes are only allowed in templates/base.html",
)
def check_css_rules(errors: list[str]) -> None:
for path in sorted(CSS_DIR.rglob("*.css")):
is_tokens = path.name == "tokens.css"
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if not is_tokens and HEX_RE.search(line):
err(errors, path, idx, RULE_HEX_OUTSIDE_TOKENS, "hex colors are only allowed in tokens.css")
if not is_tokens and PX_SPACING_RE.search(line):
err(errors, path, idx, RULE_PX_SPACING, "hard px spacing is forbidden outside tokens.css")
def main() -> int:
errors: list[str] = []
check_base_assets(errors)
check_pages_extend_base(errors)
check_templates_inline_and_assets(errors)
check_css_rules(errors)
if errors:
print("Policy checks failed:")
for item in errors:
print(f"{item}")
print("Hint: fix violations or move token values into app/static/css/tokens.css")
return 1
print("Policy checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())