archy_contracts
Check Python imports against architectural contracts after any edit. Catches violations of layered dependencies, forbidden imports, and cycle rules before they degrade the codebase.
Instructions
Call after any Python edit that adds, removes, or changes an import statement, especially across package boundaries. A failed contract means the new import violates the architecture - revert or restructure before continuing. Runs import-linter contracts (transitive Layers, Forbidden, Independence, Protected, AcyclicSiblings); stricter than archy_check, which only catches direct edges between layers in archy.yaml. Reads .importlinter (or pyproject.toml). Requires pip install archy[contracts].
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| config_path | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| available | Yes | ||
| error | No | ||
| all_kept | No | ||
| kept | No | ||
| broken | No | ||
| module_count | No | ||
| import_count | No | ||
| contracts | No |
Implementation Reference
- src/archy/mcp.py:347-367 (registration)Registration of the 'archy_contracts' tool on the FastMCP server via @server.tool decorator.
@server.tool( name="archy_contracts", description=( "**Call after any Python edit that adds, removes, or changes an " "import statement, especially across package boundaries.** A " "failed contract means the new import violates the architecture - " "revert or restructure before continuing. Runs import-linter " "contracts (transitive Layers, Forbidden, Independence, Protected, " "AcyclicSiblings); stricter than archy_check, which only catches " "direct edges between layers in archy.yaml. Reads .importlinter " "(or pyproject.toml). Requires `pip install archy[contracts]`." ), ) def archy_contracts( path: str, config_path: str | None = None, ) -> ContractsPayload: return _run_contracts( Path(path), config_filename=Path(config_path) if config_path else None, ) - src/archy/mcp.py:610-632 (handler)The _run_contracts internal handler that delegates to run_contracts from archy.contracts and wraps the result into a ContractsPayload.
def _run_contracts(path: Path, *, config_filename: Path | None) -> ContractsPayload: from archy.contracts import ( ContractsConfigError, ContractsNotAvailable, run_contracts, ) try: result = run_contracts(path, config_filename=config_filename) except ContractsNotAvailable as exc: return ContractsPayload(available=False, error=str(exc)) except ContractsConfigError as exc: return ContractsPayload(available=True, error=str(exc)) return ContractsPayload( available=True, all_kept=result.all_kept, kept=result.kept, broken=result.broken, module_count=result.module_count, import_count=result.import_count, contracts=result.contracts, ) - src/archy/mcp.py:130-141 (schema)The ContractsPayload response model for the archy_contracts tool, defining the shape returned to the MCP client.
class ContractsPayload(BaseModel): model_config = ConfigDict(frozen=True) available: bool error: str | None = None all_kept: bool | None = None kept: int | None = None broken: int | None = None module_count: int | None = None import_count: int | None = None contracts: tuple[ContractCheck, ...] = () - src/archy/contracts.py:46-82 (helper)ContractCheck and ContractsResult pydantic models that define the contract data structure used by archy_contracts.
class ContractCheck(BaseModel): """One contract's result. `metadata` is the import-linter contract-type- specific shape (e.g., `invalid_chains` for ForbiddenContract); kept opaque here so the wrap doesn't need to know every contract type's schema.""" model_config = ConfigDict(frozen=True) name: str contract_type: str kept: bool metadata: dict[str, object] warnings: tuple[str, ...] class ContractsResult(BaseModel): model_config = ConfigDict(frozen=True) kept: int broken: int module_count: int import_count: int contracts: tuple[ContractCheck, ...] = () @property def all_kept(self) -> bool: return self.broken == 0 class ContractsNotAvailable(RuntimeError): """Raised when `import-linter` is not installed.""" class ContractsConfigError(RuntimeError): """Raised when the .importlinter file is missing or invalid.""" def run_contracts( - src/archy/contracts.py:82-145 (helper)The run_contracts function that resolves config and drives import-linter, with config resolution order: explicit config_filename, .importlinter, or archy.yaml fallback.
def run_contracts( project_dir: Path, config_filename: str | Path | None = None, ) -> ContractsResult: """Run import-linter against `project_dir` and return a structured result. `project_dir` must contain (or be the parent of) an importable copy of the package(s) named in the config; import-linter's graph builder uses runtime `import` resolution. Config resolution: `config_filename` (if given) wins; otherwise prefers `.importlinter` in `project_dir`; otherwise falls back to translating `archy.yaml` into Forbidden contracts. Raises `ContractsConfigError` if none are present. """ try: from importlinter import configuration as _configuration # noqa: F401 except ImportError as exc: raise ContractsNotAvailable( "import-linter is not installed. " "Install with `pip install archy[contracts]` to use this feature." ) from exc project_dir = project_dir.resolve() if config_filename is not None: config_path = Path(config_filename).resolve() if not config_path.exists(): raise ContractsConfigError(f"contracts config not found: {config_path}") with _ProjectOnSysPath(project_dir): return _drive_import_linter(config_path=config_path) importlinter_path = project_dir / ".importlinter" if importlinter_path.exists(): with _ProjectOnSysPath(project_dir): return _drive_import_linter(config_path=importlinter_path) archy_yaml_path = project_dir / "archy.yaml" if archy_yaml_path.exists(): try: user_options = _archy_yaml_to_user_options(archy_yaml_path) except LayerConfigError as exc: raise ContractsConfigError( f"could not derive contracts from {archy_yaml_path}: {exc}" ) from exc warnings.warn( "deriving transitive contracts from archy.yaml `forbid:` is a best-effort " "fallback and cannot express ignore_imports / whitelisted edges. For any " "project that needs to allow legitimate transitive paths, add a " ".importlinter file (the canonical contracts config). See " "https://import-linter.readthedocs.io/en/stable/contract_types.html", UserWarning, stacklevel=2, ) with _ProjectOnSysPath(project_dir): return _drive_import_linter(user_options=user_options) raise ContractsConfigError( f"no contracts config found in {project_dir}: expected `.importlinter` " "(canonical, supports all five contract types and ignore_imports whitelists) " "or `archy.yaml` (best-effort fallback that translates `forbid:` rules to " "transitive Forbidden contracts). See " "https://import-linter.readthedocs.io/en/stable/contract_types.html" )