Skip to content

Index

axm_audit

axm-audit: Code auditing and quality rules for Python projects.

This package provides comprehensive project auditing capabilities including: - Structure validation (files, directories) - Quality checks (linting, type checking, complexity) - Security analysis (Bandit integration, secrets detection) - Dependency scanning (pip-audit, deptry) - Test coverage enforcement (pytest-cov) - Architecture analysis (circular imports, god classes, coupling) - Best practices enforcement (docstrings, security patterns)

Example

from axm_audit import audit_project from pathlib import Path

result = audit_project(Path(".")) print(f"Score: {result.quality_score}/100 — Grade {result.grade}") Score: 95.0/100 — Grade A

AuditResult

Bases: BaseModel

Aggregated result of a project audit.

Contains all individual check results and computed summary. quality_score and grade may be passed explicitly (e.g. in tests); otherwise they are computed from checks automatically.

Source code in packages/axm-audit/src/axm_audit/models/results.py
Python
class AuditResult(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Aggregated result of a project audit.

    Contains all individual check results and computed summary.
    ``quality_score`` and ``grade`` may be passed explicitly (e.g. in
    tests); otherwise they are computed from checks automatically.
    """

    project_path: str | None = Field(
        default=None, description="Path to the audited project"
    )
    checks: list[CheckResult] = Field(default_factory=list)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def success(self) -> bool:
        """True if all checks passed."""
        return all(c.passed for c in self.checks)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def total(self) -> int:
        """Total number of checks."""
        return len(self.checks)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def failed(self) -> int:
        """Number of failed checks."""
        return sum(1 for c in self.checks if not c.passed)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def quality_score(self) -> float | None:
        """Weighted average across 9 code-quality categories.

        Categories and weights:
            Linting (15%), Type Safety (15%), Complexity (15%),
            Testing (10%), Test Quality (10%), Security (10%),
            Dependencies (10%), Architecture (10%), Practices (5%).

        Structure and tooling emit findings but are NOT scored
        (structure is handled by axm-init; tooling is informational).
        Returns None if no scored checks are present.
        """
        category_scores = collect_category_scores(self.checks)
        if not category_scores:
            return None

        # Weighted average: avg each category, then weight.
        # Normalize by sum of present weights so filtered audits
        # (e.g. category="lint") are not penalized for missing categories.
        total = sum(
            (sum(scores) / len(scores)) * _CATEGORY_WEIGHTS[cat]
            for cat, scores in category_scores.items()
        )
        weight_sum = sum(_CATEGORY_WEIGHTS[cat] for cat in category_scores)
        if weight_sum <= 0:
            return None
        return round(total / weight_sum, 1)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def grade(self) -> str | None:
        """Letter grade derived from quality_score.

        A >= 90, B >= 80, C >= 70, D >= 60, F < 60.
        Returns None if quality_score is None.
        """
        score = self.quality_score
        if score is None:
            return None
        _thresholds = [
            (_GRADE_A, "A"),
            (_GRADE_B, "B"),
            (_GRADE_C, "C"),
            (_GRADE_D, "D"),
        ]
        return next((g for t, g in _thresholds if score >= t), "F")

    model_config = ConfigDict(extra="forbid")
failed property

Number of failed checks.

grade property

Letter grade derived from quality_score.

A >= 90, B >= 80, C >= 70, D >= 60, F < 60. Returns None if quality_score is None.

quality_score property

Weighted average across 9 code-quality categories.

Categories and weights

Linting (15%), Type Safety (15%), Complexity (15%), Testing (10%), Test Quality (10%), Security (10%), Dependencies (10%), Architecture (10%), Practices (5%).

Structure and tooling emit findings but are NOT scored (structure is handled by axm-init; tooling is informational). Returns None if no scored checks are present.

success property

True if all checks passed.

total property

Total number of checks.

CheckResult

Bases: BaseModel

Result of a single compliance check.

Designed for machine parsing by AI Agents.

Source code in packages/axm-audit/src/axm_audit/models/results.py
Python
class CheckResult(BaseModel):  # type: ignore[explicit-any]  # pydantic synthesizes __init__(**data: Any)
    """Result of a single compliance check.

    Designed for machine parsing by AI Agents.
    """

    rule_id: str = Field(..., description="Unique identifier for the rule")
    passed: bool = Field(..., description="Whether the check passed")
    message: str = Field(..., description="Human-readable result message")
    severity: Severity = Field(default=Severity.ERROR, description="Severity level")
    details: dict[str, object] | None = Field(
        default=None, description="Structured data (cycles, metrics)"
    )
    text: str | None = Field(
        default=None, description="Pre-rendered detail text for display"
    )
    fix_hint: str | None = Field(default=None, description="Actionable fix suggestion")
    category: str | None = Field(
        default=None, description="Scoring category (injected by auditor)"
    )
    metadata: dict[str, object] = Field(
        default_factory=dict,
        description="Rule-specific structured payload (clusters, verdicts, ...)",
    )
    score: int | None = Field(
        default=None,
        ge=0,
        le=100,
        description="Numeric score in [0, 100] for scored categories",
    )

    model_config = ConfigDict(extra="forbid")

Severity

Bases: StrEnum

Severity level for check results.

Source code in packages/axm-audit/src/axm_audit/models/results.py
Python
class Severity(StrEnum):
    """Severity level for check results."""

    ERROR = "error"  # Blocks audit pass
    WARNING = "warning"  # Non-blocking issue
    INFO = "info"  # Informational only

audit_project(project_path, category=None, quick=False)

Audit a project against Python 2026 standards.

Rules execute in parallel via ThreadPoolExecutor for speed. Each rule is isolated — one failure does not prevent others. An ASTCache is shared across rules to avoid redundant parsing.

Parameters:

Name Type Description Default
project_path Path

Root directory of the project to audit.

required
category str | None

Optional category filter.

None
quick bool

If True, run only lint + type checks.

False

Returns:

Type Description
AuditResult

AuditResult containing all check results.

Raises:

Type Description
FileNotFoundError

If project_path does not exist.

Source code in packages/axm-audit/src/axm_audit/core/auditor.py
Python
def audit_project(
    project_path: Path,
    category: str | None = None,
    quick: bool = False,
) -> AuditResult:
    """Audit a project against Python 2026 standards.

    Rules execute in parallel via ThreadPoolExecutor for speed.
    Each rule is isolated — one failure does not prevent others.
    An ``ASTCache`` is shared across rules to avoid redundant parsing.

    Args:
        project_path: Root directory of the project to audit.
        category: Optional category filter.
        quick: If True, run only lint + type checks.

    Returns:
        AuditResult containing all check results.

    Raises:
        FileNotFoundError: If project_path does not exist.
    """
    if not project_path.exists():
        raise FileNotFoundError(f"Project path does not exist: {project_path}")

    workspace_packages = iter_workspace_packages(project_path)
    if workspace_packages:
        return _audit_workspace(
            project_path, workspace_packages, category=category, quick=quick
        )

    rules = get_rules_for_category(category, quick)

    cache = ASTCache()
    token = set_ast_cache(cache)
    try:
        with concurrent.futures.ThreadPoolExecutor() as pool:
            futures = [
                pool.submit(
                    contextvars.copy_context().run, _safe_check, rule, project_path
                )
                for rule in rules
            ]
            checks = [f.result() for f in futures]
    finally:
        reset_ast_cache(token)

    return AuditResult(project_path=str(project_path), checks=checks)

get_rules_for_category(category, quick=False)

Get rules for a specific category or all rules.

Parameters:

Name Type Description Default
category str | None

Filter to specific category, or None for all.

required
quick bool

If True, only lint + type checks.

False

Returns:

Type Description
list[ProjectRule]

List of rule instances to run.

Raises:

Type Description
ValueError

If category is not valid.

Source code in packages/axm-audit/src/axm_audit/core/auditor.py
Python
def get_rules_for_category(
    category: str | None, quick: bool = False
) -> list[ProjectRule]:
    """Get rules for a specific category or all rules.

    Args:
        category: Filter to specific category, or None for all.
        quick: If True, only lint + type checks.

    Returns:
        List of rule instances to run.

    Raises:
        ValueError: If category is not valid.
    """
    _ensure_registry_loaded()

    if quick:
        from axm_audit.core.rules.quality import LintingRule, TypeCheckRule

        return [LintingRule(), TypeCheckRule()]

    # Validate category
    if category is not None and category not in VALID_CATEGORIES:
        raise ValueError(
            f"Invalid category: {category}. "
            f"Valid categories: {', '.join(sorted(VALID_CATEGORIES))}"
        )

    if not category:
        return _build_all_rules()

    registry = get_registry()
    rule_classes = registry.get(category, [])
    rules: list[ProjectRule] = []
    for cls in rule_classes:
        rules.extend(cls.get_instances())
    return rules