spec: Layered Architecture Boundary Policy — four-layer model, permitted patterns, violation detection [AUTO-ARCH-5] #8581
@@ -46795,6 +46795,331 @@ The following table shows which Protocol each pipeline slot implements and what
|
||||
| **Pipeline: PreambleGenerator** | `config.toml` `context.pipeline.preamble-generator` or project/plan YAML | TOML/YAML + Python module | Configuration-driven (scope chain) |
|
||||
| **Pipeline: SkeletonCompressor** | `config.toml` `context.pipeline.skeleton-compressor` or project/plan YAML | TOML/YAML + Python module | Configuration-driven (scope chain) |
|
||||
|
||||
## Layered Architecture Boundary Policy (Cross-Cutting)
|
||||
|
||||
### Overview
|
||||
|
||||
CleverAgents enforces a strict layered architecture. Each layer has defined responsibilities and permitted dependencies. Cross-layer imports that violate these boundaries are **architectural violations** that must be detected and remediated.
|
||||
|
||||
This policy formalizes the architecture described in [ADR-049](adr/ADR-049-layered-architecture-boundary-enforcement.md) and provides the canonical reference for all architectural boundary decisions. It applies to every module in the codebase and is enforced by the architecture test suite.
|
||||
|
||||
!!! danger "Zero-Tolerance Policy"
|
||||
Architectural boundary violations are **blocking defects**. A PR that introduces a new violation must not be merged until the violation is remediated or an explicit ADR exception is recorded.
|
||||
|
||||
!!! adr "Architecture Decision"
|
||||
See [ADR-049: Layered Architecture Boundary Enforcement Policy](adr/ADR-049-layered-architecture-boundary-enforcement.md) for the full decision record, rationale, and migration guidance.
|
||||
|
||||
### The Five Layers
|
||||
|
||||
CleverAgents defines five layers. Dependencies flow strictly **inward** (toward the domain): outer layers may import from inner layers, but never the reverse.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Presentation (CLI / TUI) │
|
||||
│ ↓ imports from Application only │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Application │
|
||||
│ ↓ imports from Domain + Infrastructure (via interface) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Domain │
|
||||
│ ↓ imports from Shared + stdlib only │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure │
|
||||
│ ↓ implements Domain interfaces; imports sqlalchemy etc │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Shared (cleveragents.shared) │
|
||||
│ ↓ no internal imports; pure DTOs, exceptions, types │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Layer 1: CLI (`cleveragents.cli`)
|
||||
|
||||
- **Responsibility**: Parse user input, format output, invoke Application layer services.
|
||||
- **Permitted imports from**: `cleveragents.application` (services only), `cleveragents.shared` (DTOs, exceptions).
|
||||
- **Forbidden imports from**: `cleveragents.domain`, `cleveragents.infrastructure`, `sqlalchemy`, `alembic`, any database driver.
|
||||
- **Pattern**: CLI commands receive DTOs from Application services and render them. They never touch domain objects or database connections directly.
|
||||
|
||||
!!! example "Correct CLI Pattern"
|
||||
```python
|
||||
# cleveragents/cli/commands/decisions.py
|
||||
from cleveragents.application.services import DecisionService
|
||||
from cleveragents.shared.dto import DecisionDTO
|
||||
|
||||
def list_decisions(ctx: typer.Context) -> None:
|
||||
svc: DecisionService = ctx.obj.decision_service
|
||||
decisions: list[DecisionDTO] = svc.list_decisions()
|
||||
render_table(decisions)
|
||||
```
|
||||
|
||||
!!! failure "Violation — CLI importing sqlalchemy"
|
||||
```python
|
||||
# cleveragents/cli/commands/system.py ← VIOLATION (issue #8386)
|
||||
from sqlalchemy.exc import IntegrityError # ← forbidden in CLI layer
|
||||
```
|
||||
|
||||
#### Layer 2: Application (`cleveragents.application`)
|
||||
|
||||
- **Responsibility**: Orchestrate domain operations, implement use cases, manage transactions.
|
||||
- **Permitted imports from**: `cleveragents.domain`, `cleveragents.infrastructure` (via interfaces only), `cleveragents.shared`.
|
||||
- **Forbidden imports from**: `cleveragents.cli`, `cleveragents.tui`.
|
||||
- **Pattern**: Application services accept and return DTOs. They call domain objects and infrastructure repositories. They never call CLI or TUI code.
|
||||
|
||||
#### Layer 3: Domain (`cleveragents.domain`, `cleveragents.decisions`, `cleveragents.invariants`, `cleveragents.plans`)
|
||||
|
||||
- **Responsibility**: Business logic, domain entities, domain services, repository interfaces.
|
||||
- **Permitted imports from**: `cleveragents.shared`, Python standard library, pure domain dependencies (no third-party persistence libraries).
|
||||
- **Forbidden imports from**: `cleveragents.cli`, `cleveragents.tui`, `cleveragents.application`, `cleveragents.infrastructure`, `sqlalchemy`, any ORM or database driver.
|
||||
- **Pattern**: Domain objects are pure Python. They define repository interfaces (as `Protocol` or `ABC`) but never implement them. The domain layer is the most stable layer and must remain free of infrastructure concerns.
|
||||
|
||||
!!! example "Correct Domain Repository Interface"
|
||||
```python
|
||||
# cleveragents/domain/repositories.py
|
||||
from typing import Protocol
|
||||
from cleveragents.domain.entities import Decision
|
||||
|
||||
class DecisionRepository(Protocol):
|
||||
def get(self, decision_id: str) -> Decision | None: ...
|
||||
def save(self, decision: Decision) -> None: ...
|
||||
def list_by_plan(self, plan_id: str) -> list[Decision]: ...
|
||||
```
|
||||
|
||||
#### Layer 4: Infrastructure (`cleveragents.infrastructure`)
|
||||
|
||||
- **Responsibility**: Database access, external API clients, file system operations, caching.
|
||||
- **Permitted imports from**: `cleveragents.domain` (to implement domain interfaces), `cleveragents.shared`, `sqlalchemy`, external libraries.
|
||||
- **Forbidden imports from**: `cleveragents.cli`, `cleveragents.tui`, `cleveragents.application`.
|
||||
- **Pattern**: Infrastructure implements domain repository interfaces. It never calls application services. SQLAlchemy models live exclusively in this layer.
|
||||
|
||||
!!! example "Correct Infrastructure Implementation"
|
||||
```python
|
||||
# cleveragents/infrastructure/repositories/sql_decision_repository.py
|
||||
from sqlalchemy.orm import Session
|
||||
from cleveragents.domain.entities import Decision
|
||||
from cleveragents.domain.repositories import DecisionRepository
|
||||
|
||||
class SQLDecisionRepository:
|
||||
"""Implements DecisionRepository using SQLAlchemy."""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
self._session = session
|
||||
|
||||
def get(self, decision_id: str) -> Decision | None:
|
||||
...
|
||||
|
||||
def save(self, decision: Decision) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
#### Layer 5: TUI (`cleveragents.tui`)
|
||||
|
||||
- **Responsibility**: Text User Interface rendering, user interaction, A2A event materialization.
|
||||
- **Permitted imports from**: `cleveragents.application` (services only), `cleveragents.shared`, `cleveragents.a2a.client`.
|
||||
- **Forbidden imports from**: `cleveragents.domain`, `cleveragents.infrastructure`, `sqlalchemy`.
|
||||
- **Pattern**: TUI is parallel to CLI. It uses Application services and the A2A client, never domain or infrastructure directly. TUI widgets receive DTOs and render them.
|
||||
|
||||
### Dependency Inversion Pattern
|
||||
|
||||
When a higher layer needs a capability from a lower layer without creating a forbidden import, apply the **Dependency Inversion Principle**:
|
||||
|
||||
1. Define an interface (`Protocol` or `ABC`) in the **domain layer** or `cleveragents.shared`.
|
||||
2. Implement the interface in the **infrastructure layer**.
|
||||
3. Inject the implementation via constructor injection or the dependency injection container at application startup.
|
||||
|
||||
This is the only approved mechanism for crossing layer boundaries in the "upward" direction.
|
||||
|
||||
```python
|
||||
# Step 1: Interface in domain layer
|
||||
# cleveragents/domain/repositories.py
|
||||
class DecisionRepository(Protocol):
|
||||
def save(self, decision: Decision) -> None: ...
|
||||
|
||||
# Step 2: Implementation in infrastructure layer
|
||||
# cleveragents/infrastructure/repositories/sql_decision_repository.py
|
||||
class SQLDecisionRepository:
|
||||
def save(self, decision: Decision) -> None:
|
||||
... # SQLAlchemy implementation
|
||||
|
||||
# Step 3: Injection at application startup
|
||||
# cleveragents/application/container.py
|
||||
from cleveragents.infrastructure.repositories.sql_decision_repository import SQLDecisionRepository
|
||||
|
||||
container.register(DecisionRepository, SQLDecisionRepository)
|
||||
```
|
||||
|
||||
### Permitted Cross-Layer Patterns
|
||||
|
||||
The following patterns are explicitly approved for crossing layer boundaries:
|
||||
|
||||
| Pattern | Description | Example |
|
||||
|:--------|:------------|:--------|
|
||||
| **Dependency Inversion** | Interface defined in domain, implementation in infrastructure; injected by application | `DecisionRepository` (domain interface) → `SQLDecisionRepository` (infrastructure impl) |
|
||||
| **Exception Wrapping** | Infrastructure exceptions caught and re-raised as domain or application exceptions | `sqlalchemy.exc.IntegrityError` → `DuplicateDecisionError` (domain exception) |
|
||||
| **DTO Translation** | Application layer translates domain objects to DTOs for consumption by CLI/TUI | `Decision` (domain entity) → `DecisionDTO` (shared DTO) |
|
||||
| **Event Publishing** | Domain events published via an interface defined in the domain layer, consumed by the application layer | `DomainEventPublisher` (domain interface) → `ApplicationEventBus` (application impl) |
|
||||
|
||||
!!! warning "Exception Wrapping Must Happen at the Boundary"
|
||||
Infrastructure exceptions must be caught and wrapped **inside the infrastructure layer** before propagating to the application layer. Application and domain code must never catch `sqlalchemy` exceptions directly.
|
||||
|
||||
### Violation Detection
|
||||
|
||||
The architecture test suite at `tests/architecture/` enforces these boundaries automatically. All boundary rules must be expressed as executable tests using `import-linter` or `pytest` with AST-based import analysis.
|
||||
|
||||
#### Required Architecture Tests
|
||||
|
||||
```python
|
||||
# tests/architecture/test_layer_boundaries.py
|
||||
"""
|
||||
Architecture boundary enforcement tests.
|
||||
These tests fail if any module violates the layered import rules.
|
||||
Run with: pytest tests/architecture/
|
||||
"""
|
||||
|
||||
def test_cli_does_not_import_sqlalchemy():
|
||||
"""CLI layer must not import sqlalchemy directly.
|
||||
|
||||
Violation example: issue #8386 — cleveragents.cli.commands.system
|
||||
imports sqlalchemy.exc.IntegrityError directly.
|
||||
"""
|
||||
...
|
||||
|
||||
def test_cli_does_not_import_domain():
|
||||
"""CLI layer must not import domain modules directly.
|
||||
|
||||
CLI must use Application services and receive DTOs.
|
||||
Direct domain imports bypass the application layer.
|
||||
"""
|
||||
...
|
||||
|
||||
def test_cli_does_not_import_infrastructure():
|
||||
"""CLI layer must not import infrastructure modules."""
|
||||
...
|
||||
|
||||
def test_tui_does_not_import_sqlalchemy():
|
||||
"""TUI layer must not import sqlalchemy directly."""
|
||||
...
|
||||
|
||||
def test_tui_does_not_import_domain():
|
||||
"""TUI layer must not import domain modules directly."""
|
||||
...
|
||||
|
||||
def test_tui_does_not_import_infrastructure():
|
||||
"""TUI layer must not import infrastructure modules."""
|
||||
...
|
||||
|
||||
def test_domain_does_not_import_infrastructure():
|
||||
"""Domain layer must not import infrastructure modules.
|
||||
|
||||
Domain defines interfaces; infrastructure implements them.
|
||||
"""
|
||||
...
|
||||
|
||||
def test_domain_does_not_import_sqlalchemy():
|
||||
"""Domain layer must not import sqlalchemy or any ORM."""
|
||||
...
|
||||
|
||||
def test_domain_does_not_import_application():
|
||||
"""Domain layer must not import application modules."""
|
||||
...
|
||||
|
||||
def test_infrastructure_does_not_import_application():
|
||||
"""Infrastructure layer must not import application modules."""
|
||||
...
|
||||
|
||||
def test_infrastructure_does_not_import_cli():
|
||||
"""Infrastructure layer must not import CLI modules."""
|
||||
...
|
||||
|
||||
def test_infrastructure_does_not_import_tui():
|
||||
"""Infrastructure layer must not import TUI modules."""
|
||||
...
|
||||
```
|
||||
|
||||
#### import-linter Configuration
|
||||
|
||||
```ini
|
||||
# .importlinter
|
||||
[importlinter]
|
||||
root_package = cleveragents
|
||||
|
||||
[importlinter:contract:cli-layer]
|
||||
name = CLI layer must not import domain or infrastructure
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.cli
|
||||
forbidden_modules =
|
||||
cleveragents.domain
|
||||
cleveragents.infrastructure
|
||||
sqlalchemy
|
||||
alembic
|
||||
|
||||
[importlinter:contract:tui-layer]
|
||||
name = TUI layer must not import domain or infrastructure
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.tui
|
||||
forbidden_modules =
|
||||
cleveragents.domain
|
||||
cleveragents.infrastructure
|
||||
sqlalchemy
|
||||
|
||||
[importlinter:contract:domain-layer]
|
||||
name = Domain layer must not import infrastructure or presentation
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.domain
|
||||
cleveragents.decisions
|
||||
cleveragents.invariants
|
||||
cleveragents.plans
|
||||
forbidden_modules =
|
||||
cleveragents.cli
|
||||
cleveragents.tui
|
||||
cleveragents.application
|
||||
cleveragents.infrastructure
|
||||
sqlalchemy
|
||||
alembic
|
||||
|
||||
[importlinter:contract:infrastructure-layer]
|
||||
name = Infrastructure layer must not import presentation or application
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.infrastructure
|
||||
forbidden_modules =
|
||||
cleveragents.cli
|
||||
cleveragents.tui
|
||||
cleveragents.application
|
||||
```
|
||||
|
||||
### Known Violations
|
||||
|
||||
The following violations are tracked and must be remediated before the next minor release:
|
||||
|
||||
| Issue | Module | Violation | Remediation | Status |
|
||||
|:------|:-------|:----------|:------------|:-------|
|
||||
| [#8386](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/8386) | `cleveragents.cli.commands.system` | Imports `sqlalchemy.exc.IntegrityError` directly — CLI→Infrastructure violation | Replace direct sqlalchemy import with application-layer exception type (e.g., `cleveragents.shared.exceptions.DuplicateEntityError`) | Open (v3.2.0) |
|
||||
|
||||
!!! note "Tracking New Violations"
|
||||
When a new violation is discovered (e.g., by a failing architecture test or code review), open an issue with the label `arch-violation` and reference it in this table. The issue must be resolved before the milestone that contains the violating module is closed.
|
||||
|
||||
### Remediation Checklist
|
||||
|
||||
When an architectural boundary violation is detected, follow this checklist:
|
||||
|
||||
1. **Identify** the violating import — which module imports what.
|
||||
2. **Classify** the violation — which layer boundary is crossed (e.g., CLI→Infrastructure, Domain→Infrastructure).
|
||||
3. **Design the fix** — determine whether the solution requires:
|
||||
- Moving the import to the correct layer,
|
||||
- Introducing a new interface (Protocol/ABC) in the domain or shared layer,
|
||||
- Wrapping an infrastructure exception as a domain/application exception, or
|
||||
- Injecting a dependency rather than importing it directly.
|
||||
4. **Implement** the interface in the correct layer.
|
||||
5. **Update** the violating module to use the interface or the correct exception type.
|
||||
6. **Add an architecture test** to `tests/architecture/` to prevent regression.
|
||||
7. **Reference** the violation issue in the PR description.
|
||||
8. **Update** the Known Violations table above to mark the issue as resolved.
|
||||
|
||||
!!! tip "When in Doubt"
|
||||
If you are unsure whether an import is permitted, ask: *"Does this import cause a higher layer to depend on a lower layer's implementation detail?"* If yes, apply dependency inversion. If the answer is still unclear, open a discussion issue and tag it `arch-question`.
|
||||
|
||||
## Milestone Plan
|
||||
|
||||
This section defines the ordered milestone plan for CleverAgents v3.x, mapping architectural features to verifiable deliverables. Each milestone builds on the previous and is independently testable. Milestones v3.0.0 and v3.1.0 are **complete**. This plan covers v3.2.0 through v3.7.0 — the production-ready target.
|
||||
|
||||
Reference in New Issue
Block a user