|
# 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"
|