Skip to content

Checker

checker

Check engine โ€” orchestrates all checks and produces ProjectResult.

CheckEngine

Orchestrates project checks and produces results.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
class CheckEngine:
    """Orchestrates project checks and produces results."""

    def __init__(
        self,
        project_path: Path,
        *,
        category: str | None = None,
        framework: Framework | str | None = None,
    ) -> None:
        validate_context_tables()
        self.project_path = project_path.resolve()
        self.category = category
        self.framework = (
            detect_framework(self.project_path)
            if framework is None
            else Framework(framework)
        )
        self.context = detect_context(self.project_path)
        if self.context == ProjectContext.EXPERIMENT or category == "experiment":
            raise ValueError(
                "Experiment validation is owned by axm-lab; install axm-lab "
                "and use its experiment_check tool."
            )
        if self.context == ProjectContext.PAPER or category == "paper":
            raise ValueError("Paper validation is owned by axm-lab; use paper_check.")
        self.workspace_root = find_workspace_root(self.project_path)

    def _is_excluded(self, check_name: str, exclusions: set[str]) -> bool:
        """Check if a check name matches any exclusion prefix."""
        return any(check_name.startswith(prefix) for prefix in exclusions)

    def _should_skip(self, check_name: str | None) -> bool:
        """Return True if the check should be skipped for context reasons."""
        return check_name in SKIP_BY_CONTEXT.get(self.context, frozenset())

    def _should_redirect(self, check_name: str | None) -> bool:
        """Return True if the check should be redirected to workspace root."""
        return (
            check_name in REDIRECT_BY_CONTEXT.get(self.context, frozenset())
            and self.workspace_root is not None
        )

    def _filter_checks(
        self,
        checks_to_run: dict[str, list[Callable[[Path], CheckResult]]],
    ) -> list[Callable[[Path], CheckResult]]:
        """Apply context-aware skip and redirect filtering.

        Skip and redirect decisions key off the canonical
        ``category.fn_name`` name (:func:`get_check_name`, the same
        convention used by the context tables). Exclusions
        are NOT handled here: they match against ``CheckResult.name`` after
        the check runs โ€” but that name is now re-stamped with the SAME
        canonical value (see :func:`stamp_canonical_name`), so excluding by
        the displayed name actually skips the check.
        """
        all_fns: list[Callable[[Path], CheckResult]] = []

        for fns in checks_to_run.values():
            for fn in fns:
                check_name = get_check_name(fn)
                if self._should_skip(check_name):
                    continue
                if self._should_redirect(check_name):
                    all_fns.append(_redirect_to_root(fn, self.workspace_root))  # type: ignore[arg-type]
                else:
                    all_fns.append(fn)

        return all_fns

    def _apply_exclusions(
        self,
        results: list[CheckResult],
        exclusions: set[str],
    ) -> tuple[list[CheckResult], list[str]]:
        """Split run results into kept + excluded using the canonical name.

        Exclusion matching keys off ``CheckResult.name`` โ€” which ``run`` has
        already re-stamped to the canonical :func:`get_check_name` form, the
        same convention used by the context tables and shown
        in the report. Excluding by the displayed name therefore actually
        skips the check. Excluded checks become auto-pass results carrying
        that same canonical name.
        """
        if not exclusions:
            return results, []

        kept: list[CheckResult] = []
        excluded_names: list[str] = []
        for result in results:
            if self._is_excluded(result.name, exclusions):
                kept.append(_make_excluded_result(result.name, result.category))
                excluded_names.append(result.name)
            else:
                kept.append(result)
        return kept, excluded_names

    def run(self) -> ProjectResult:
        """Run all checks (or filtered by category) for the project's framework."""
        registry = CHECKS_BY_FRAMEWORK.get(self.framework, ALL_CHECKS)
        if self.category:
            category_checks = registry.get(self.category)
            if category_checks is None:
                category_checks = _discover_explicit_category(self.category)
            if category_checks is None:
                valid = ", ".join(sorted(registry.keys()))
                msg = (
                    f"Unknown category '{self.category}' for framework "
                    f"'{self.framework.value}'. Valid: {valid}"
                )
                raise ValueError(msg)
            checks_to_run = {self.category: category_checks}
        else:
            checks_to_run = registry

        exclusions = load_exclusions(self.project_path)
        all_fns = self._filter_checks(checks_to_run)

        members = (
            _protocol_members(self.project_path)
            if self.category == "protocols" and self.context == ProjectContext.WORKSPACE
            else []
        )
        with ThreadPoolExecutor(max_workers=8) as pool:
            raw_results = list(pool.map(lambda fn: fn(self.project_path), all_fns))
            if members:
                raw_results = list(
                    pool.map(
                        lambda pair: _aggregate_member_check(pair[0], pair[1], members),
                        zip(all_fns, raw_results, strict=True),
                    )
                )

        # Single source of truth: re-stamp every result with the canonical
        # name (``get_check_name``) so SKIP / REDIRECT / exclude / display
        # all key off the SAME string (AXM-2046).
        results = [
            stamp_canonical_name(fn, result)
            for fn, result in zip(all_fns, raw_results, strict=True)
        ]

        results = _category_inventory_findings(self.project_path, members, results)
        results, excluded_names = self._apply_exclusions(results, exclusions)

        return ProjectResult.from_checks(
            self.project_path,
            results,
            context=self.context.value,
            workspace_root=self.workspace_root,
            excluded_checks=excluded_names,
        )
run()

Run all checks (or filtered by category) for the project's framework.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def run(self) -> ProjectResult:
    """Run all checks (or filtered by category) for the project's framework."""
    registry = CHECKS_BY_FRAMEWORK.get(self.framework, ALL_CHECKS)
    if self.category:
        category_checks = registry.get(self.category)
        if category_checks is None:
            category_checks = _discover_explicit_category(self.category)
        if category_checks is None:
            valid = ", ".join(sorted(registry.keys()))
            msg = (
                f"Unknown category '{self.category}' for framework "
                f"'{self.framework.value}'. Valid: {valid}"
            )
            raise ValueError(msg)
        checks_to_run = {self.category: category_checks}
    else:
        checks_to_run = registry

    exclusions = load_exclusions(self.project_path)
    all_fns = self._filter_checks(checks_to_run)

    members = (
        _protocol_members(self.project_path)
        if self.category == "protocols" and self.context == ProjectContext.WORKSPACE
        else []
    )
    with ThreadPoolExecutor(max_workers=8) as pool:
        raw_results = list(pool.map(lambda fn: fn(self.project_path), all_fns))
        if members:
            raw_results = list(
                pool.map(
                    lambda pair: _aggregate_member_check(pair[0], pair[1], members),
                    zip(all_fns, raw_results, strict=True),
                )
            )

    # Single source of truth: re-stamp every result with the canonical
    # name (``get_check_name``) so SKIP / REDIRECT / exclude / display
    # all key off the SAME string (AXM-2046).
    results = [
        stamp_canonical_name(fn, result)
        for fn, result in zip(all_fns, raw_results, strict=True)
    ]

    results = _category_inventory_findings(self.project_path, members, results)
    results, excluded_names = self._apply_exclusions(results, exclusions)

    return ProjectResult.from_checks(
        self.project_path,
        results,
        context=self.context.value,
        workspace_root=self.workspace_root,
        excluded_checks=excluded_names,
    )

format_agent(result)

Agent-optimized output: passed_count=N, failed=full detail.

Minimizes tokens by replacing the full passed-check list with a count. Only failures carry actionable detail.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def format_agent(result: ProjectResult) -> dict[str, object]:
    """Agent-optimized output: passed_count=N, failed=full detail.

    Minimizes tokens by replacing the full passed-check list with a count.
    Only failures carry actionable detail.
    """
    return {
        "score": None if result.not_applicable else result.score,
        "grade": None if result.not_applicable else result.grade.value,
        "context": result.context,
        "workspace_root": str(result.workspace_root) if result.workspace_root else None,
        "excluded_checks": result.excluded_checks,
        "passed_count": sum(1 for c in result.checks if c.passed),
        "failures": [
            {
                "name": f.name,
                "message": f.message,
                "details": f.details,
                "fix": f.fix,
            }
            for f in result.failures
        ],
    }

format_agent_text(result)

Agent-optimized text rendering of a check result.

Compact companion to :func:format_agent: a one-line header with score, grade, context and pass/fail counts, then one block per failed check carrying its message, every detail and the full fix verbatim. Passed checks are summarized as a count (they carry no actionable remedy).

The structured :func:format_agent dict remains the source of truth for programmatic consumers; this string is what the LLM reads.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def format_agent_text(result: ProjectResult) -> str:
    """Agent-optimized text rendering of a check result.

    Compact companion to :func:`format_agent`: a one-line header with score,
    grade, context and pass/fail counts, then one block per failed check
    carrying its message, every detail and the full fix verbatim. Passed
    checks are summarized as a count (they carry no actionable remedy).

    The structured :func:`format_agent` dict remains the source of truth for
    programmatic consumers; this string is what the LLM reads.
    """
    passed = sum(1 for c in result.checks if c.passed)
    failures = result.failures
    context = result.context or "package"
    # A not-applicable run (no weighted checks scored in this context) renders
    # an N/A marker, NOT a numeric 0/100 Grade F โ€” else the LLM reads a real
    # failure where a dimension simply does not apply.
    verdict = (
        "N/A"
        if result.not_applicable
        else f"{result.grade.value} {result.score}/100 (Grade {result.grade.value})"
    )
    header = f"init_check | {verdict} | {context} | {passed} ok ยท {len(failures)} fail"
    if not failures:
        return f"{header}\nAll gold-standard checks passed."

    lines = [header, ""]
    for failure in failures:
        lines.extend(_format_agent_failure(failure))
    return "\n".join(lines)

format_json(result)

Format check result as JSON-serializable dict.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def format_json(result: ProjectResult) -> dict[str, object]:
    """Format check result as JSON-serializable dict."""
    return {
        "project": str(result.project_path),
        "score": None if result.not_applicable else result.score,
        "grade": None if result.not_applicable else result.grade.value,
        "context": result.context,
        "workspace_root": str(result.workspace_root) if result.workspace_root else None,
        "excluded_checks": result.excluded_checks,
        "categories": {
            cat: {"earned": cs.earned, "total": cs.total}
            for cat, cs in result.categories.items()
        },
        "checks": [
            {
                "name": c.name,
                "category": c.category,
                "passed": c.passed,
                "earned": c.earned,
                "weight": c.weight,
                "message": c.message,
            }
            for c in result.checks
        ],
        "failures": [
            {
                "name": f.name,
                "weight": f.weight,
                "message": f.message,
                "details": f.details,
                "fix": f.fix,
            }
            for f in result.failures
        ],
    }

format_report(result, *, verbose=False)

Format check result as human-readable report.

Parameters:

Name Type Description Default
result ProjectResult

Project check result.

required
verbose bool

If True, list every individual check. If False (default), only show summary for passing categories and detail for failures.

False
Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def format_report(result: ProjectResult, *, verbose: bool = False) -> str:
    """Format check result as human-readable report.

    Args:
        result: Project check result.
        verbose: If True, list every individual check.
            If False (default), only show summary for passing categories
            and detail for failures.
    """
    lines: list[str] = [
        f"๐Ÿ“‹ AXM Check โ€” {result.project_path.name}",
        f"   Path: {result.project_path}",
    ]

    if result.context:
        ctx_line = f"   Context: {result.context.upper()}"
        if result.workspace_root:
            ctx_line += f" (root: {result.workspace_root})"
        lines.append(ctx_line)

    lines.append("")

    # Category breakdown
    for cat_name, cat_score in result.categories.items():
        cat_checks = [c for c in result.checks if c.category == cat_name]
        lines.append(f"  {cat_name} ({cat_score.earned}/{cat_score.total})")
        lines.extend(_format_category_checks(cat_checks, verbose=verbose))
        lines.append("")

    # Score โ€” a not-applicable run (no weighted checks scored in this
    # context, e.g. `check --category workspace` on a standalone project)
    # renders an N/A line, NOT a numeric 0/100 Grade F.
    if result.not_applicable:
        lines.append(
            "  Category not applicable (N/A) โ€” no checks scored in this context"
        )
    else:
        grade_emoji = {"A": "๐Ÿ†", "B": "โœ…", "C": "โš ๏ธ", "D": "๐Ÿ”ง", "F": "โŒ"}
        emoji = grade_emoji.get(result.grade.value, "")
        lines.append(
            f"  Score: {result.score}/100 โ€” Grade {result.grade.value} {emoji}"
        )
    lines.append("")

    # Failures
    if result.failures:
        lines.extend(_format_failures(result.failures))

    return "\n".join(lines)

get_check_name(fn)

Derive the canonical check name from the function's module + name.

This is THE single source of truth for check naming. The convention is category.function_name_without_check_ (the module name is the category). The same string is used by the context tables SKIP_BY_CONTEXT / REDIRECT_BY_CONTEXT (pre-run, on the function), by [tool.axm-init].exclude matching (post-run, on the result), and as the displayed CheckResult.name โ€” so a name shown in the report can always be excluded by config.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def get_check_name(fn: Callable[[Path], CheckResult]) -> str | None:
    """Derive the canonical check name from the function's module + name.

    This is THE single source of truth for check naming. The convention is
    ``category.function_name_without_check_`` (the module name is the
    category). The same string is used by the context tables
    ``SKIP_BY_CONTEXT`` / ``REDIRECT_BY_CONTEXT``
    (pre-run, on the function), by ``[tool.axm-init].exclude`` matching
    (post-run, on the result), and as the displayed ``CheckResult.name`` โ€”
    so a name shown in the report can always be excluded by config.
    """
    module = getattr(fn, "__module__", "")
    category = module.rsplit(".", 1)[-1] if module else ""
    fn_name = getattr(fn, "__name__", "")
    if fn_name.startswith("check_"):
        return f"{category}.{fn_name[6:]}"
    return None

protocol_status(result)

Project explicit declared state independently of conformity verdicts.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def protocol_status(result: ProjectResult) -> list[dict[str, str | bool]]:
    """Project explicit declared state independently of conformity verdicts."""
    from axm_init.checks.protocols import protocol_inventory

    trusted = _status_trusted(result)
    return [
        {
            "member": member,
            "graph_name": record.graph_name,
            "state": record.state,
            "location": record.location,
            "validated": record.state == "ready" and trusted,
            "executable": record.state == "ready" and trusted,
        }
        for member, path in _status_projects(result)
        for record in protocol_inventory(path)
    ]

resolve_exit_code(result)

Resolve the CLI process exit code for a check result.

A not-applicable verdict (no weighted checks ran for this context โ€” e.g. a category that does not apply to the project) is a skip/success and exits 0. An applicable run exits 0 only on a perfect score, otherwise 1 โ€” so a real 0/100 Grade F still fails the process.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def resolve_exit_code(result: ProjectResult) -> int:
    """Resolve the CLI process exit code for a check *result*.

    A not-applicable verdict (no weighted checks ran for this context โ€” e.g.
    a category that does not apply to the project) is a skip/success and
    exits ``0``. An applicable run exits ``0`` only on a perfect score,
    otherwise ``1`` โ€” so a real 0/100 Grade F still fails the process.
    """
    if result.not_applicable:
        return 0
    return 0 if result.score >= 100 else 1

stamp_canonical_name(fn, result)

Re-stamp a result with the canonical name derived from its function.

Check functions historically hand-set CheckResult.name with ad-hoc strings that sometimes dropped a redundant category prefix (e.g. ci.workflow_exists instead of ci.ci_workflow_exists). To keep ONE convention across SKIP / REDIRECT / exclude / display, every result is re-stamped here with :func:get_check_name โ€” the same value the skip/redirect filters key off. When the name cannot be inferred (function not named check_*), the result's own name is kept.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def stamp_canonical_name(
    fn: Callable[[Path], CheckResult],
    result: CheckResult,
) -> CheckResult:
    """Re-stamp a result with the canonical name derived from its function.

    Check functions historically hand-set ``CheckResult.name`` with ad-hoc
    strings that sometimes dropped a redundant category prefix
    (e.g. ``ci.workflow_exists`` instead of ``ci.ci_workflow_exists``). To
    keep ONE convention across SKIP / REDIRECT / exclude / display, every
    result is re-stamped here with :func:`get_check_name` โ€” the same value
    the skip/redirect filters key off. When the name cannot be inferred
    (function not named ``check_*``), the result's own name is kept.
    """
    canonical = get_check_name(fn)
    if canonical is None or canonical == result.name:
        return result
    return result.model_copy(update={"name": canonical})

validate_context_tables()

Check every context-table id against the discovered check registry.

Raises:

Type Description
ValueError

if a table holds an id no registered check declares โ€” the message names every offending id.

Source code in packages/axm-init/src/axm_init/core/checker.py
Python
def validate_context_tables() -> None:
    """Check every context-table id against the discovered check registry.

    Raises:
        ValueError: if a table holds an id no registered check declares โ€”
            the message names every offending id.
    """
    known = _known_check_ids()
    unknown = sorted(
        check_id
        for table in (SKIP_BY_CONTEXT, REDIRECT_BY_CONTEXT)
        for ids in table.values()
        for check_id in ids
        if check_id not in known
    )
    if unknown:
        listed = ", ".join(repr(check_id) for check_id in unknown)
        msg = f"Unknown check id(s) in the context tables: {listed}"
        raise ValueError(msg)