Skip to content

Python API

The root exports below are the package's external Python surface. Rule implementation modules, formatters, tools and the witness are separately importable but are not re-exported at the root.

The monorepo build already generates per-module pages under reference/axm_audit/. This curated page renders in both standalone and monorepo builds and makes the entry points discoverable in navigation.

Audit entry points

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

Audit a project against its ecosystem's 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
framework Framework | str | None

Ecosystem to audit against. None auto-detects from the project's manifest markers (:func:detect_framework); pass an explicit value to override.

None

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,
    framework: Framework | str | None = None,
) -> AuditResult:
    """Audit a project against its ecosystem's 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.
        framework: Ecosystem to audit against. ``None`` auto-detects from the
            project's manifest markers (:func:`detect_framework`); pass an
            explicit value to override.

    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}")

    fw = detect_framework(project_path) if framework is None else Framework(framework)

    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, framework=fw)

    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, framework=Framework.PYTHON)

Get rules for a specific category or all rules, scoped to framework.

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
framework Framework

Ecosystem whose rules to return (default python keeps the historical behaviour for callers that don't pass it).

PYTHON

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,
    framework: Framework = Framework.PYTHON,
) -> list[ProjectRule]:
    """Get rules for a specific category or all rules, scoped to *framework*.

    Args:
        category: Filter to specific category, or None for all.
        quick: If True, only lint + type checks.
        framework: Ecosystem whose rules to return (default ``python`` keeps
            the historical behaviour for callers that don't pass it).

    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_rules 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(framework)

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

Result models

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 crashed_rules(self) -> list[str]:
        """rule_ids whose check raised, in encounter order.

        Crashed rules contribute ``score=0`` to their scored category (they
        are never silently dropped); this field makes a degraded audit
        traceable rather than silent.
        """
        return [c.rule_id for c in self.checks if _is_crashed(c)]

    @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
        return grade_for_score(score)

    model_config = ConfigDict(extra="forbid")

crashed_rules property

rule_ids whose check raised, in encounter order.

Crashed rules contribute score=0 to their scored category (they are never silently dropped); this field makes a degraded audit traceable rather than silent.

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

axm_audit.__version__ is a string supplied by the build's VCS version hook.

Formatters and exceptions

formatters

Output formatters for audit results — human-readable and JSON.

format_report(result)

Format audit result as human-readable category-grouped report.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_report(result: AuditResult) -> str:
    """Format audit result as human-readable category-grouped report."""
    lines: list[str] = [
        "📋 axm-audit — Quality Audit",
        f"   Path: {result.project_path or 'unknown'}",
        "",
    ]
    lines.extend(_format_categories(result))
    lines.extend(_format_score(result))
    lines.extend(_format_improvements(result))
    lines.extend(_format_failures(result))
    return "\n".join(lines)

format_json(result)

Format audit result as JSON-serializable dict.

The score/grade pair is resolved through the single serialization source (:func:axm_audit.score.resolve_score_grade): the payload always carries a numeric score, and when a score cannot be computed at all it raises :class:axm_audit.score.ScoreIncalculableError rather than emit a success payload without a score key.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_json(result: AuditResult) -> dict[str, object]:
    """Format audit result as JSON-serializable dict.

    The ``score``/``grade`` pair is resolved through the single serialization
    source (:func:`axm_audit.score.resolve_score_grade`): the payload always
    carries a numeric ``score``, and when a score cannot be computed at all it
    raises :class:`axm_audit.score.ScoreIncalculableError` rather than emit a
    success payload without a ``score`` key.
    """
    score, grade = resolve_score_grade(result)
    return {
        "score": score,
        "grade": grade,
        "total": result.total,
        "failed": result.failed,
        "success": result.success,
        "checks": [
            {
                "rule_id": c.rule_id,
                "passed": c.passed,
                "message": c.message,
                "details": c.details,
                "metadata": c.metadata or None,
            }
            for c in result.checks
        ],
    }

format_agent(result)

Agent-optimized output: passed=summary, failed=full detail.

Minimizes tokens for passing checks while giving full context on failures. For failed checks, text and details are both included when present (None values are omitted). Passed checks that carry actionable detail (e.g. missing docstrings) are promoted to dicts. Rule-specific metadata (e.g. tautology verdicts, duplicate clusters, pyramid mismatches) is propagated verbatim under the metadata key on both passed and failed entries when non-empty.

Score/grade derive from the single serialization source (:func:axm_audit.score.score_grade_or_none); this lax surface tolerates an incalculable score as None rather than failing loud.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_agent(result: AuditResult) -> dict[str, object]:
    """Agent-optimized output: passed=summary, failed=full detail.

    Minimizes tokens for passing checks while giving full context on
    failures.  For failed checks, ``text`` and ``details`` are both included
    when present (``None`` values are omitted).  Passed checks that
    carry actionable detail (e.g. missing docstrings) are promoted to dicts.
    Rule-specific ``metadata`` (e.g. tautology verdicts, duplicate clusters,
    pyramid mismatches) is propagated verbatim under the ``metadata`` key
    on both passed and failed entries when non-empty.

    Score/grade derive from the single serialization source
    (:func:`axm_audit.score.score_grade_or_none`); this lax surface tolerates
    an incalculable score as ``None`` rather than failing loud.
    """
    score, grade = score_grade_or_none(result)
    return {
        "score": score,
        "grade": grade,
        "passed": [_render_passed_entry(c) for c in result.checks if c.passed],
        "failed": [_render_failed_entry(c) for c in result.checks if not c.passed],
    }

format_agent_text(data, category=None)

Render agent-format audit data as compact text for LLM consumption.

Consumes the dict produced by format_agent and returns a minimal text representation optimised for token count.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_agent_text(
    data: dict[str, object],
    category: str | None = None,
) -> str:
    """Render agent-format audit data as compact text for LLM consumption.

    Consumes the dict produced by ``format_agent`` and returns a minimal
    text representation optimised for token count.
    """
    score = data.get("score")
    grade = data.get("grade")
    raw_passed = data.get("passed", [])
    raw_failed = data.get("failed", [])
    passed: list[str | dict[str, object]] = (
        list(raw_passed) if isinstance(raw_passed, list) else []
    )
    failed: list[dict[str, object]] = (
        [f for f in raw_failed if isinstance(f, dict)]
        if isinstance(raw_failed, list)
        else []
    )

    cat_label = f" {category}" if category else ""
    score_part = f" {grade} {score}" if score is not None and grade is not None else ""
    header = f"audit{cat_label} |{score_part} | {len(passed)} pass · {len(failed)} fail"
    lines: list[str] = [header]

    lines.extend(_render_passed(passed))
    for f in failed:
        lines.extend(_render_failed_check(f))

    return "\n".join(lines)

format_test_quality_text(result, mismatches_only=False)

Render test-quality findings grouped by rule.

Order: private imports → pyramid → duplicates → tautologies. With mismatches_only=True only the pyramid section is emitted, filtered to entries whose folder differs from the classified level.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_test_quality_text(
    result: AuditResult,
    mismatches_only: bool = False,
) -> str:
    """Render test-quality findings grouped by rule.

    Order: private imports → pyramid → duplicates → tautologies.
    With ``mismatches_only=True`` only the pyramid section is emitted,
    filtered to entries whose folder differs from the classified level.
    """
    private, pyramid, clusters, verdicts, no_pkg, file_naming = _extract_test_quality(
        result
    )

    if mismatches_only:
        return _format_pyramid_only(pyramid)

    lines: list[str] = []
    lines.extend(_format_private_section(private))
    lines.append("")
    lines.extend(_format_pyramid_section(pyramid))
    lines.append("")
    lines.extend(_format_duplicates_section(clusters))
    lines.append("")
    lines.extend(_format_tautologies_section(verdicts))
    if no_pkg:
        lines.append("")
        lines.append("TEST_QUALITY_NO_PACKAGE_SYMBOL:")
        for entry in no_pkg:
            lines.append(
                f"  [{entry.get('verdict', '?')}] {entry.get('test_file', '?')}"
            )
    if file_naming:
        lines.append("")
        lines.append("TEST_QUALITY_FILE_NAMING:")
        for entry in file_naming:
            lines.append(
                f"  [{entry.get('verdict', '?')}] "
                f"{entry.get('current_name') or entry.get('canonical_name', '?')} "
                f"→ {entry.get('proposed_name', '?')}"
            )

    return "\n".join(lines)

format_test_quality_json(result)

JSON superset: clusters + verdicts + pyramid + private violations.

Source code in packages/axm-audit/src/axm_audit/formatters.py
Python
def format_test_quality_json(result: AuditResult) -> dict[str, object]:
    """JSON superset: clusters + verdicts + pyramid + private violations."""
    private, pyramid, clusters, verdicts, no_pkg, file_naming = _extract_test_quality(
        result
    )
    rule_ids = sorted(
        {
            c.rule_id
            for c in result.checks
            if (c.rule_id or "").startswith("TEST_QUALITY_")
        }
    )
    score, grade = score_grade_or_none(result)
    payload: dict[str, object] = {
        "score": score,
        "grade": grade,
        "rules": rule_ids,
        "clusters": clusters,
        "verdicts": verdicts,
        "pyramid_mismatches": pyramid,
        "private_import_violations": private,
        "no_package_symbol": [
            {**entry, "rule_id": "TEST_QUALITY_NO_PACKAGE_SYMBOL"} for entry in no_pkg
        ],
        "file_naming": [
            {**entry, "rule_id": "TEST_QUALITY_FILE_NAMING"} for entry in file_naming
        ],
    }
    return payload

ScoreIncalculableError

Bases: RuntimeError

Raised when an audit yields no measurable scored signal (N/A).

Covers both an audit with no scored-category check at all and one whose scored-category checks are every one not-applicable (score=None). Signals that a success payload without a score must NOT be emitted; the strict callers (audit --json) fail loud instead of dropping the key silently or reporting a misleading 0/F.

Source code in packages/axm-audit/src/axm_audit/score.py
Python
class ScoreIncalculableError(RuntimeError):
    """Raised when an audit yields no measurable scored signal (N/A).

    Covers both an audit with no scored-category check at all and one whose
    scored-category checks are every one not-applicable (``score=None``).
    Signals that a success payload without a ``score`` must NOT be emitted; the
    strict callers (``audit --json``) fail loud instead of dropping the key
    silently or reporting a misleading 0/F.
    """

Tool implementations

execute(*, path='.', category=None, **kwargs)

Audit a Python project's code quality.

Parameters:

Name Type Description Default
path str

Path to project root.

'.'
category str | None

Optional category filter. One of:

None

Returns:

Type Description
ToolResult

ToolResult with audit scores and details (data dict

ToolResult

and a compact text summary for LLM consumption).

Source code in packages/axm-audit/src/axm_audit/tools/audit.py
Python
def execute(
    self,
    *,
    path: str = ".",
    category: str | None = None,
    **kwargs: object,
) -> ToolResult:
    """Audit a Python project's code quality.

    Args:
        path: Path to project root.
        category: Optional category filter. One of:
            {categories}

    Returns:
        ToolResult with audit scores and details (``data`` dict
        and a compact ``text`` summary for LLM consumption).
    """
    try:
        project_path = Path(path).resolve()
        if not project_path.is_dir():
            return ToolResult(
                success=False, error=f"Not a directory: {project_path}"
            )

        from axm_audit.core.auditor import audit_project
        from axm_audit.formatters import format_agent, format_agent_text

        result = audit_project(project_path, category=category)
        data = format_agent(result)
        text = format_agent_text(data, category=category)

        from axm_audit.code_metrics import collect_code_metrics
        from axm_audit.quality_trace import record_quality_snapshot

        record_quality_snapshot(path=str(project_path), kind="audit", data=data)
        # Also snapshot code metrics (LOC + structural counts) so trends can
        # be charted over time; best-effort, never breaks the audit.
        record_quality_snapshot(
            path=str(project_path),
            kind="code",
            data=collect_code_metrics(str(project_path)),
        )
        return ToolResult(success=True, data=data, text=text)
    except Exception as exc:  # noqa: BLE001
        return ToolResult(success=False, error=str(exc))

execute(*, path='.', mode='failures', files=None, markers=None, stop_on_first=True, include_cases=False, **kwargs)

Run tests with structured output.

Parameters:

Name Type Description Default
path str

Path to project root.

'.'
mode str

"cases" requests lossless per-item evidence through the historical field; other values remain backward-compatible.

'failures'
files list[str] | None

Specific test files to run.

None
markers list[str] | None

Pytest markers to filter.

None
stop_on_first bool

Stop on first failure.

True
include_cases bool

Include lossless per-item pytest verdicts.

False

Returns:

Type Description
ToolResult

ToolResult with structured test report.

Source code in packages/axm-audit/src/axm_audit/tools/audit_test.py
Python
def execute(  # noqa: PLR0913
    self,
    *,
    path: str = ".",
    mode: str = "failures",
    files: list[str] | None = None,
    markers: list[str] | None = None,
    stop_on_first: bool = True,
    include_cases: bool = False,
    **kwargs: object,
) -> ToolResult:
    """Run tests with structured output.

    Args:
        path: Path to project root.
        mode: ``"cases"`` requests lossless per-item evidence through the
            historical field; other values remain backward-compatible.
        files: Specific test files to run.
        markers: Pytest markers to filter.
        stop_on_first: Stop on first failure.
        include_cases: Include lossless per-item pytest verdicts.

    Returns:
        ToolResult with structured test report.
    """
    include_case_evidence = include_cases or mode == "cases"
    if mode not in {"failures", "cases"}:
        logger.info("mode param is deprecated")

    try:
        project_path = Path(path).resolve()
        if not project_path.is_dir():
            return ToolResult(
                success=False, error=f"Not a directory: {project_path}"
            )

        from axm_audit.core.test_runner import run_tests

        report = run_tests(
            project_path,
            files=files,
            markers=markers,
            stop_on_first=stop_on_first,
            include_cases=include_case_evidence,
        )

        data = dataclasses.asdict(report)
        if include_case_evidence:
            data["cases"] = [dataclasses.asdict(case) for case in report.cases]
        else:
            data.pop("cases", None)

        from axm_audit.tools.audit_test_text import format_audit_test_text

        text = format_audit_test_text(report)

        return ToolResult(success=_tool_succeeded(report), data=data, text=text)
    except Exception as exc:  # noqa: BLE001
        return ToolResult(success=False, error=str(exc))

execute(*, path='.', apply=False, rules=None, **kwargs)

Run the fix pipeline on a project.

Parameters:

Name Type Description Default
path str

Path to project root.

'.'
apply bool

If True, mutate the tree; otherwise dry-run.

False
rules list[str] | None

Optional list of rule ids to filter the pipeline.

None

Returns:

Type Description
ToolResult

ToolResult with a JSON-serializable data dict and a

ToolResult

human-readable text summary.

Source code in packages/axm-audit/src/axm_audit/tools/audit_fix.py
Python
def execute(
    self,
    *,
    path: str = ".",
    apply: bool = False,
    rules: list[str] | None = None,
    **kwargs: object,
) -> ToolResult:
    """Run the fix pipeline on a project.

    Args:
        path: Path to project root.
        apply: If True, mutate the tree; otherwise dry-run.
        rules: Optional list of rule ids to filter the pipeline.

    Returns:
        ToolResult with a JSON-serializable ``data`` dict and a
        human-readable ``text`` summary.
    """
    try:
        project_path = Path(path).resolve()
        if not project_path.is_dir():
            return ToolResult(
                success=False, error=f"Not a directory: {project_path}"
            )

        from axm_audit.core.fix import run
        from axm_audit.core.fix.report import format_report

        rules_set = set(rules) if rules is not None else None
        report = run(project_path, apply=apply, rules=rules_set)

        data = _report_to_dict(report)
        text = format_report(report, project_path)

        return ToolResult(success=True, data=data, text=text)
    except Exception as exc:  # noqa: BLE001
        return ToolResult(success=False, error=str(exc))

execute(*, path='.', timeout=_DEFAULT_TIMEOUT, **kwargs)

Run the documentation gate on the target package.

Parameters:

Name Type Description Default
path str

Path to the package root holding mkdocs.yml.

'.'
timeout int

Hard wall-clock bound (seconds) for the mkdocs subprocess.

_DEFAULT_TIMEOUT

Returns:

Type Description
ToolResult

ToolResult with structured findings (data) plus a human summary

ToolResult

(text) on success, or success=False with a clear error when

ToolResult

mkdocs is absent, times out, or the build fails without findings.

Source code in packages/axm-audit/src/axm_audit/doc_gate/tool.py
Python
def execute(
    self,
    *,
    path: str = ".",
    timeout: int = _DEFAULT_TIMEOUT,
    **kwargs: object,
) -> ToolResult:
    """Run the documentation gate on the target package.

    Args:
        path: Path to the package root holding ``mkdocs.yml``.
        timeout: Hard wall-clock bound (seconds) for the mkdocs subprocess.

    Returns:
        ToolResult with structured findings (``data``) plus a human summary
        (``text``) on success, or ``success=False`` with a clear error when
        mkdocs is absent, times out, or the build fails without findings.
    """
    try:
        project_path = Path(path).resolve()
        if not project_path.is_dir():
            return ToolResult(
                success=False, error=f"Not a directory: {project_path}"
            )
        with tempfile.TemporaryDirectory() as site_dir:
            completed = subprocess.run(  # noqa: S603
                ["mkdocs", "build", "--strict", "--site-dir", site_dir],  # noqa: S607
                cwd=project_path,
                capture_output=True,
                text=True,
                timeout=timeout,
                check=False,
            )
    except FileNotFoundError:
        return ToolResult(
            success=False,
            error="mkdocs is not installed in the environment "
            "(binary not found on PATH)",
        )
    except subprocess.TimeoutExpired:
        return ToolResult(
            success=False,
            error=f"mkdocs build timed out after {timeout}s",
        )
    except OSError as exc:
        return ToolResult(success=False, error=str(exc))

    output = (completed.stdout or "") + (completed.stderr or "")
    findings = [] if completed.returncode == 0 else parse_mkdocs_output(output)
    if completed.returncode != 0 and not findings:
        detail = output.strip() or "no output captured"
        return ToolResult(
            success=False,
            error=f"mkdocs build failed (exit {completed.returncode}): {detail}",
        )
    data = {
        "findings": [finding.model_dump(mode="json") for finding in findings],
        "count": len(findings),
    }
    return ToolResult(success=True, data=data, text=_summarize(findings))

Witness

AuditQualityRule dataclass

Run audit checks and return structured feedback.

Attributes:

Name Type Description
categories list[str]

Audit categories to run (default: lint + type).

working_dir str

Project root to audit.

guidance str | None

Optional extra guidance appended on failure.

Source code in packages/axm-audit/src/axm_audit/witnesses/audit_quality.py
Python
@dataclass
class AuditQualityRule:
    """Run audit checks and return structured feedback.

    Attributes:
        categories: Audit categories to run (default: lint + type).
        working_dir: Project root to audit.
        guidance: Optional extra guidance appended on failure.
    """

    categories: list[str] = field(default_factory=lambda: ["lint", "type"])
    working_dir: str = "."
    guidance: str | None = None
    scope: str = "."
    exclude_rules: list[str] = field(default_factory=list)
    extra_dirs: list[str] = field(default_factory=list)

    def _audit_extra_dirs(
        self,
        categories: list[str],
        results: list[AuditResult],
    ) -> None:
        """Run audit categories on each extra directory."""
        for extra_dir in self.extra_dirs:
            extra_path = Path(extra_dir).resolve()
            if not extra_path.is_dir():
                logger.info("Skipping extra_dir: %s does not exist", extra_dir)
                continue
            for category in categories:
                try:
                    result = audit_project(extra_path, category=category)
                    results.append(result)
                except Exception:
                    logger.exception(
                        "audit_project failed for extra_dir=%s category=%s",
                        extra_dir,
                        category,
                    )

    def _run_categories(
        self,
        project_path: Path,
        categories: list[str],
    ) -> list[AuditResult]:
        """Run each audit category independently, collecting results."""
        results: list[AuditResult] = []
        for category in categories:
            try:
                result = audit_project(project_path, category=category)
                results.append(result)
            except Exception:
                logger.exception("audit_project failed for category=%s", category)
        return results

    def _filter_excluded(
        self, failed_items: list[dict[str, object]]
    ) -> list[dict[str, object]]:
        """Remove items whose rule_id matches any exclude prefix."""
        if not self.exclude_rules or not failed_items:
            return failed_items
        return [
            item
            for item in failed_items
            if not any(
                str(item.get("rule_id", "")).startswith(prefix)
                for prefix in self.exclude_rules
            )
        ]

    def _build_failure_result(
        self,
        agent_output: dict[str, object],
        failed_items: list[dict[str, object]],
    ) -> WitnessResult:
        """Build a WitnessResult.failure with structured feedback."""
        why_lines = json.dumps(failed_items, indent=2, ensure_ascii=False)
        how = (
            "Fix each violation listed above. Lint errors: fix the code "
            "(do NOT add # noqa). Type errors: fix the types "
            "(do NOT add # type: ignore without verifying)."
        )
        if self.guidance:
            how = f"{how}\n\n{self.guidance}"
        return WitnessResult.failure(
            feedback=ValidationFeedback(
                what=f"Quality gate failed: {len(failed_items)} violation(s)",
                why=why_lines,
                how=how,
            ),
            metadata={"audit": agent_output},
        )

    def _resolve_project_path(self, kwargs: dict[str, object]) -> Path:
        """Resolve the working directory from kwargs or instance default."""
        working_dir_param = kwargs.get("working_dir")
        working_dir = (
            working_dir_param
            if isinstance(working_dir_param, str)
            else self.working_dir
        )
        return Path(working_dir).resolve()

    @staticmethod
    def _coerce_failed_items(raw_failed: object) -> list[dict[str, object]]:
        """Filter ``raw_failed`` to a list of dicts, dropping non-dict entries."""
        if not isinstance(raw_failed, list):
            return []
        return [item for item in raw_failed if isinstance(item, dict)]

    def _aggregate_audit_output(
        self,
        results: list[AuditResult],
        categories: list[str],
    ) -> tuple[dict[str, object], list[dict[str, object]]]:
        """Merge category results, run extra_dirs audits, and filter exclusions."""
        self._audit_extra_dirs(categories, results)
        all_checks: list[object] = []
        for r in results:
            all_checks.extend(r.checks)
        merged = AuditResult(checks=all_checks)
        agent_output = format_agent(merged)
        failed_input = self._coerce_failed_items(agent_output.get("failed", []))
        failed_items = self._filter_excluded(failed_input)
        agent_output["failed"] = failed_items
        return agent_output, failed_items

    def validate(self, content: str, **kwargs: object) -> WitnessResult:
        """Run audit categories and aggregate results.

        Each category runs independently — failures in one do not
        prevent execution of the others.
        """
        project_path = self._resolve_project_path(kwargs)
        if not project_path.is_dir():
            return WitnessResult.failure(
                feedback=ValidationFeedback(
                    what="Invalid working directory",
                    why=f"Not a directory: {project_path}",
                    how="Ensure the witness params.working_dir points to "
                    "a valid project root.",
                ),
            )

        unknown = [c for c in self.categories if c not in VALID_CATEGORIES]
        if unknown:
            return WitnessResult.failure(
                feedback=ValidationFeedback(
                    what=f"Unknown audit category/categories: {unknown}",
                    why=(
                        "The witness was configured with categories the "
                        "auditor does not know how to run, so nothing would "
                        "be audited. A quality gate MUST fail loud on a "
                        "config error rather than pass green having checked "
                        "nothing."
                    ),
                    how=(
                        "Fix the witness `params.categories`. Valid "
                        f"categories: {', '.join(sorted(VALID_CATEGORIES))}."
                    ),
                ),
            )
        categories = list(self.categories)
        if not categories:
            return WitnessResult.failure(
                feedback=ValidationFeedback(
                    what="No audit categories configured",
                    why=(
                        "The witness `params.categories` is empty, so the "
                        "gate would audit nothing and pass green — a "
                        "false-green a quality gate must never emit."
                    ),
                    how=(
                        "Set at least one category. Valid categories: "
                        f"{', '.join(sorted(VALID_CATEGORIES))}."
                    ),
                ),
            )

        results = self._run_categories(project_path, categories)
        if not results:
            return WitnessResult.failure(
                feedback=ValidationFeedback(
                    what="All audit categories failed to execute",
                    why="audit_project raised exceptions for every category.",
                    how="Check the project structure and audit configuration.",
                ),
            )

        agent_output, failed_items = self._aggregate_audit_output(results, categories)
        if not failed_items:
            return WitnessResult.success(metadata={"audit": agent_output})
        return self._build_failure_result(agent_output, failed_items)

validate(content, **kwargs)

Run audit categories and aggregate results.

Each category runs independently — failures in one do not prevent execution of the others.

Source code in packages/axm-audit/src/axm_audit/witnesses/audit_quality.py
Python
def validate(self, content: str, **kwargs: object) -> WitnessResult:
    """Run audit categories and aggregate results.

    Each category runs independently — failures in one do not
    prevent execution of the others.
    """
    project_path = self._resolve_project_path(kwargs)
    if not project_path.is_dir():
        return WitnessResult.failure(
            feedback=ValidationFeedback(
                what="Invalid working directory",
                why=f"Not a directory: {project_path}",
                how="Ensure the witness params.working_dir points to "
                "a valid project root.",
            ),
        )

    unknown = [c for c in self.categories if c not in VALID_CATEGORIES]
    if unknown:
        return WitnessResult.failure(
            feedback=ValidationFeedback(
                what=f"Unknown audit category/categories: {unknown}",
                why=(
                    "The witness was configured with categories the "
                    "auditor does not know how to run, so nothing would "
                    "be audited. A quality gate MUST fail loud on a "
                    "config error rather than pass green having checked "
                    "nothing."
                ),
                how=(
                    "Fix the witness `params.categories`. Valid "
                    f"categories: {', '.join(sorted(VALID_CATEGORIES))}."
                ),
            ),
        )
    categories = list(self.categories)
    if not categories:
        return WitnessResult.failure(
            feedback=ValidationFeedback(
                what="No audit categories configured",
                why=(
                    "The witness `params.categories` is empty, so the "
                    "gate would audit nothing and pass green — a "
                    "false-green a quality gate must never emit."
                ),
                how=(
                    "Set at least one category. Valid categories: "
                    f"{', '.join(sorted(VALID_CATEGORIES))}."
                ),
            ),
        )

    results = self._run_categories(project_path, categories)
    if not results:
        return WitnessResult.failure(
            feedback=ValidationFeedback(
                what="All audit categories failed to execute",
                why="audit_project raised exceptions for every category.",
                how="Check the project structure and audit configuration.",
            ),
        )

    agent_output, failed_items = self._aggregate_audit_output(results, categories)
    if not failed_items:
        return WitnessResult.success(metadata={"audit": agent_output})
    return self._build_failure_result(agent_output, failed_items)

For exact transport behavior, parameter tables and known limits, use the CLI/tools reference, results guide and framework reference. Source docstrings can lag the runtime; the narrative describes the checked behavior.