# retoor <retoor@molodetz.nl>
from __future__ import annotations
from .base import (
PAGE_CHECKS,
SITE_CHECKS,
SEVERITY_WEIGHT,
STATUS_CREDIT,
INFO,
SKIP,
Check,
PageContext,
SiteContext,
)
from . import crawl # noqa: F401
from . import meta # noqa: F401
from . import headings # noqa: F401
from . import links # noqa: F401
from . import structured_data # noqa: F401
from . import social # noqa: F401
from . import performance # noqa: F401
from . import mobile_a11y # noqa: F401
from . import security # noqa: F401
from . import ai_readiness # noqa: F401
from . import crosspage # noqa: F401
def _collect(result: object) -> list[Check]:
if result is None:
return []
if isinstance(result, Check):
return [result]
return [c for c in result if isinstance(c, Check)]
def run_page_checks(page: PageContext, site: SiteContext) -> list[Check]:
checks: list[Check] = []
for func in PAGE_CHECKS:
try:
checks.extend(_collect(func(page, site)))
except Exception as exc: # noqa: BLE001 - one bad check never aborts the page
checks.append(
Check(
id=f"{func.__name__}.error",
category="internal",
title=f"Check {func.__name__} failed",
status=INFO,
severity="info",
value=str(exc)[:200],
url=page.url,
)
)
return checks
def run_site_checks(site: SiteContext) -> list[Check]:
checks: list[Check] = []
for func in SITE_CHECKS:
try:
checks.extend(_collect(func(site)))
except Exception as exc: # noqa: BLE001
checks.append(
Check(
id=f"{func.__name__}.error",
category="internal",
title=f"Site check {func.__name__} failed",
status=INFO,
severity="info",
value=str(exc)[:200],
)
)
return checks
def compute_score(checks: list[Check]) -> dict:
by_category: dict[str, dict] = {}
earned = 0.0
weight = 0.0
counts = {"pass": 0, "warn": 0, "fail": 0, "info": 0, "skip": 0}
for check in checks:
counts[check.status] = counts.get(check.status, 0) + 1
bucket = by_category.setdefault(
check.category,
{"earned": 0.0, "weight": 0.0, "pass": 0, "warn": 0, "fail": 0, "info": 0, "skip": 0},
)
bucket[check.status] = bucket.get(check.status, 0) + 1
if check.status in (INFO, SKIP):
continue
w = SEVERITY_WEIGHT.get(check.severity, 1.0)
if w <= 0:
continue
credit = STATUS_CREDIT.get(check.status, 0.0)
earned += w * credit
weight += w
bucket["earned"] += w * credit
bucket["weight"] += w
categories = {}
for name, bucket in by_category.items():
cat_score = (
round(100 * bucket["earned"] / bucket["weight"])
if bucket["weight"] > 0
else None
)
categories[name] = {
"score": cat_score,
"pass": bucket["pass"],
"warn": bucket["warn"],
"fail": bucket["fail"],
"info": bucket["info"],
"skip": bucket["skip"],
}
score = round(100 * earned / weight) if weight > 0 else 0
return {
"score": score,
"grade": _grade(score),
"counts": counts,
"categories": categories,
}
def _grade(score: int) -> str:
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
if score >= 55:
return "D"
return "F"