#!/usr/bin/env python3 """Static-site sanity checker for the Astrologue website. Walks every .html file under the given root, then verifies: 1. every src/href asset reference resolves to a real file (site-root- relative paths such as /developer/ resolve against the site root); 2. no external URLs are referenced (the site must be self-contained); 3. internal page anchors (#id and page#id) point at real ids; 4. every local page is reachable by some link (orphan check); 5. quick HTML well-formedness signals (basic tag balance). Exit code 1 on any failure. Usage: check.py [root] """ import re import sys from pathlib import Path from urllib.parse import urlparse, unquote ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() EXTERNAL = re.compile(r"^(https?|ftp|wss?|//):?") # Reference designs are not part of the deployable site. EXCLUDE_DIRS = {"Original", "Proposed", ".git"} # Site-root-relative pages that are linked ahead of their existence because a # later task in this goal builds them. Remove entries here once the page lands # so the checker enforces them again. # A5 (locksmith, vallama, usm) runs in parallel with A4; the goal agent strips # the remaining entries at merge. The web-stack pages (astralis, statum, spry) # have landed and are enforced. UPCOMING_PAGES = { "developer/locksmith/", "developer/vallama/", "developer/usm/", } def check() -> int: errors: list[str] = [] pages = sorted(p for p in ROOT.rglob("*.html") if not (set(p.relative_to(ROOT).parts[:-1]) & EXCLUDE_DIRS)) if not pages: errors.append("no .html files found under root") ids_by_page: dict[Path, set[str]] = {} refs: list[tuple[Path, str, str]] = [] # page, attr value, kind linked_pages: set[Path] = set() for page in pages: text = page.read_text(encoding="utf-8") ids_by_page[page] = set(re.findall(r'id="([^"]+)"', text)) # strip comments so commented-out template rows are not treated as live refs body = re.sub(r"", "", text, flags=re.S) for attr, val in re.findall(r'\b(src|href)="([^"]+)"', body): refs.append((page, val, attr)) # naive tag-balance check for the elements we actually use for tag in ("html", "head", "body", "header", "nav", "main", "footer", "section", "article", "figure", "table", "ul", "ol", "div", "a", "p"): opens = len(re.findall(rf"<{tag}[\s>]", body)) closes = len(re.findall(rf"", body)) if opens != closes: errors.append(f"{page.name}: <{tag}> open/close mismatch ({opens}/{closes})") for page, val, _attr in refs: parsed = urlparse(val) if parsed.scheme or val.startswith("//"): if not (val.startswith("data:")): errors.append(f"{page.name}: EXTERNAL reference '{val}'") continue if parsed.fragment and not parsed.path: # same-page anchor if parsed.fragment not in ids_by_page[page]: errors.append(f"{page.name}: broken in-page anchor '#{parsed.fragment}'") continue target = (page.parent / unquote(parsed.path)).resolve() if parsed.path.startswith("/"): # site-root-relative: resolve against the site root, not / target = (ROOT / unquote(parsed.path).lstrip("/")).resolve() if not target.exists(): # allowlist upcoming pages no matter whether they are linked # root-relative (/developer/spry/) or relatively (spry/) try: rel = target.relative_to(ROOT).as_posix().strip("/") + "/" except ValueError: rel = None if rel is not None and rel in UPCOMING_PAGES: continue errors.append(f"{page.name}: missing asset '{val}'") continue if target.suffix == ".html" or target.is_dir(): # a page link; check its fragment if present linked_pages.add(target / "index.html" if target.is_dir() else target) if parsed.fragment: tp = target if target.suffix == ".html" else target / "index.html" if tp.is_file(): if parsed.fragment not in ids_by_page.get(tp, set()): errors.append(f"{page.name}: broken anchor in {val}") # orphan check for page in pages: if page not in linked_pages and page != ROOT / "index.html": errors.append(f"orphan page (nothing links to it): {page.relative_to(ROOT)}") if errors: print(f"FAIL ({len(errors)} problems)") for e in errors: print(" -", e) return 1 print(f"OK: {len(pages)} page(s), {len(refs)} reference(s), all local, all resolve") return 0 if __name__ == "__main__": sys.exit(check())