Skip to content

Module API

The AuditModule base class defines the interface that every audit module must implement. All modules live in scripts/modules/ and are registered via the @register_module decorator.


AuditModule Base Class

from abc import ABC, abstractmethod
from typing import Any


class AuditModule(ABC):
    MODULE_ID: str = ""
    DISPLAY_NAME: str = ""
    ALWAYS_ENABLED: bool = False

    def __init__(self):
        self.findings: list[dict] = []
        self.score_data: dict[str, Any] = {}

    @abstractmethod
    def analyse(self, html: str, url: str = "", headers: dict = None, **kwargs) -> dict:
        ...

    @abstractmethod
    def score(self, analysis: dict) -> dict:
        ...

    @classmethod
    def detect(cls, html: str) -> bool:
        return cls.ALWAYS_ENABLED

    def add_finding(self, priority: str, title: str, description: str,
                    fix: str = "", effort: str = ""):
        ...

Class Attributes

Attribute Type Description
MODULE_ID str Unique identifier used in the registry (e.g. "seo", "local_seo")
DISPLAY_NAME str Human-readable name shown in reports (e.g. "Local SEO")
ALWAYS_ENABLED bool When True, the module runs regardless of detection signals

Instance Attributes

Attribute Type Description
findings list[dict] Accumulated findings from scoring
score_data dict Arbitrary scoring metadata available after score() runs

Methods

analyse(html, url, headers, **kwargs) -> dict

Extract audit signals from HTML content.

Parameters:

Parameter Type Description
html str Raw HTML content to analyse
url str Page URL for domain extraction and link classification
headers dict Optional HTTP response headers
**kwargs Module-specific extra arguments

Returns: Dict of analysis results keyed by signal name. The structure is module-specific.

score(analysis) -> dict

Convert analysis results into a numeric score and findings list.

Parameters:

Parameter Type Description
analysis dict The dict returned by analyse()

Returns: Dict containing at least a total key with the numeric score (0--100), plus per-criterion breakdowns.

detect(html) -> bool

Class method. Determine whether this module should run for the given HTML.

The default implementation returns ALWAYS_ENABLED. Override to inspect HTML for relevant signals.

add_finding(priority, title, description, fix, effort)

Record an actionable finding during scoring.

Parameters:

Parameter Type Description
priority str "P0" (critical) through "P3" (informational)
title str Short one-line summary
description str Detailed explanation
fix str Recommended remediation
effort str "low", "medium", or "high"

Registry Functions

These are exported from scripts/modules/__init__.py:

register_module(cls)

Decorator that registers an AuditModule subclass.

from modules import register_module
from modules.base import AuditModule

@register_module
class MyModule(AuditModule):
    MODULE_ID = "my_module"
    DISPLAY_NAME = "My Module"
    ...

detect_modules(html, site_type, force_enable, force_disable) -> list[str]

Determine which modules to run. Returns a sorted list of module ID strings.

Parameter Type Description
html str HTML to scan for detection signals
site_type str \| None Site type hint (e.g. "ecommerce", "local_business")
force_enable list[str] \| None Module IDs to unconditionally enable
force_disable list[str] \| None Module IDs to unconditionally disable

get_module(module_id) -> type | None

Return the registered class for a module ID.

list_all_modules() -> list[dict]

Return metadata for every registered module. Each dict has id, name, and core keys.


Writing a Custom Module

from modules import register_module
from modules.base import AuditModule


@register_module
class MyCustomModule(AuditModule):
    MODULE_ID = "my_custom"
    DISPLAY_NAME = "My Custom Check"
    ALWAYS_ENABLED = False

    @classmethod
    def detect(cls, html: str) -> bool:
        return "my-custom-signal" in html.lower()

    def analyse(self, html: str, url: str = "", headers: dict = None, **kwargs) -> dict:
        # Extract signals from HTML
        has_feature = "my-feature" in html
        return {"has_feature": has_feature}

    def score(self, analysis: dict) -> dict:
        total = 100
        if not analysis["has_feature"]:
            total -= 20
            self.add_finding(
                priority="P2",
                title="Missing my-feature",
                description="The page lacks my-feature which improves UX.",
                fix="Add the my-feature element to the page.",
                effort="low",
            )
        return {"total": total}