Files
Aegis/.cursor/rules/aegis-architecture.md

7.2 KiB

description, globs
description globs
Aegis backend Clean Architecture rules. Apply when working on any backend Python file under backend/app/ or backend/tests/. backend/**/*.py

Aegis — Clean Modular Monolith Architecture

Architecture Overview

Aegis follows a Clean Architecture pattern inside a modular monolith. The backend has four layers with strict dependency rules:

Presentation → Application → Domain ← Infrastructure

The golden rule: dependencies only point towards the Domain layer. Infrastructure implements the ports (interfaces) defined in Domain.

Layer Structure and Rules

Domain Layer (backend/app/domain/)

The innermost layer. ZERO imports from FastAPI, SQLAlchemy, Pydantic, or any framework.

Directory Purpose
domain/enums.py Canonical domain enums (TechniqueStatus, TestState, TeamSide, TestResult)
domain/errors.py Exception hierarchy (DomainError → EntityNotFoundError, InvalidStateTransition, etc.)
domain/exceptions.py Backward-compatible re-exports from errors.py
domain/test_entity.py TestEntity — pure state machine with domain events
domain/entities/ Rich domain entities (TechniqueEntity, etc.) with business behavior
domain/value_objects/ Immutable value types (MitreId, ScoringWeights)
domain/ports/repositories/ Protocol interfaces defining data access contracts
domain/ports/services/ Protocol interfaces for external capabilities (storage, events)
domain/unit_of_work.py UnitOfWork wrapping SQLAlchemy session

NEVER import from app.models, app.routers, app.infrastructure, fastapi, or sqlalchemy inside domain/.

Application Layer (backend/app/application/ — future)

Use case orchestrators. Depends only on Domain.

Directory Purpose
application/use_cases/ One class per business operation
application/dto/ Plain data containers for use case input/output
application/interfaces/ Application-level contracts (UnitOfWork protocol)

Infrastructure Layer (backend/app/infrastructure/)

Implements ports defined in Domain. Depends on Domain and Application.

Directory Purpose
infrastructure/redis_client.py Redis connection singleton
infrastructure/persistence/repositories/ SQLAlchemy implementations of repository ports
infrastructure/persistence/mappers/ ORM model ↔ domain entity converters

Presentation Layer (routers, schemas, dependencies)

HTTP boundary. Depends on Application and Domain (for exceptions).

Directory Purpose
routers/ FastAPI routers — HTTP mapping only
schemas/ Pydantic request/response models
dependencies/ FastAPI Depends() wiring (auth, repositories)
middleware/ Error handler mapping domain exceptions → HTTP responses

Import Rules (Strict)

From \ To domain/ application/ infrastructure/ presentation/
domain/ Self only FORBIDDEN FORBIDDEN FORBIDDEN
application/ ALLOWED Self only FORBIDDEN FORBIDDEN
infrastructure/ ALLOWED (ports) ALLOWED (UoW) Self only FORBIDDEN
presentation/ ALLOWED (exceptions) ALLOWED (use cases) ALLOWED (wiring in dependencies/) Self only

How to Add a New Feature

1. Start from the Domain

  • Define or reuse domain entities in domain/entities/
  • Add value objects if needed in domain/value_objects/
  • Define repository port if a new aggregate root in domain/ports/repositories/
  • Domain exceptions go in domain/errors.py
  • Business rules live IN the entity, not in services or routers

2. Implement Infrastructure

  • Create SQLAlchemy repository implementation in infrastructure/persistence/repositories/
  • Create mapper if converting between ORM model and domain entity
  • Repository does NOT call commit() — only flush()
  • Transaction control belongs to the Unit of Work

3. Wire in Presentation

  • Add FastAPI Depends() provider in dependencies/repositories.py
  • Keep routers thin: parse request → call service/use case → return response
  • Map domain exceptions to HTTP via the error handler middleware (automatic)

4. Tests (Mandatory)

Every change MUST include tests:

  • Domain entities/value objects: pure unit tests, no DB, no mocking frameworks
  • Repositories: integration tests using the db fixture from conftest
  • Routers: API tests using the client fixture
  • At least one success test + one failure/edge-case test per behavior

Before committing, run: scripts/agent_validate_backend.sh

Existing Patterns to Follow

Domain Entity Pattern (see domain/test_entity.py)

@dataclass
class SomeEntity:
    id: uuid.UUID
    # fields...
    _events: list[DomainEvent] = field(default_factory=list, repr=False)

    @classmethod
    def from_orm(cls, model: Any) -> "SomeEntity":
        """Build from SQLAlchemy model."""
        ...

    def apply_to(self, model: Any) -> None:
        """Copy mutable fields back onto the ORM model."""
        ...

    def some_business_method(self) -> None:
        """Business logic lives HERE, not in services."""
        ...
        self._events.append(DomainEvent("something_happened"))

Repository Port Pattern (Protocol)

from typing import Protocol, runtime_checkable

@runtime_checkable
class SomeRepository(Protocol):
    def find_by_id(self, id: uuid.UUID) -> SomeEntity | None: ...
    def save(self, entity: SomeEntity) -> SomeEntity: ...

Repository Implementation Pattern

class SASomeRepository:
    def __init__(self, session: Session) -> None:
        self._session = session

    def find_by_id(self, id: uuid.UUID) -> SomeEntity | None:
        model = self._session.query(SomeModel).filter(SomeModel.id == id).first()
        return SomeMapper.to_entity(model) if model else None

    def save(self, entity: SomeEntity) -> SomeEntity:
        model = SomeMapper.to_model(entity)
        merged = self._session.merge(model)
        self._session.flush()  # NO commit — UoW does that
        return SomeMapper.to_entity(merged)

Error Handling (automatic via middleware)

Services raise domain exceptions → middleware maps to HTTP:

  • EntityNotFoundError → 404
  • DuplicateEntityError → 409
  • InvalidStateTransition → 400
  • BusinessRuleViolation → 400
  • PermissionViolation → 403

Coexistence Strategy

Old code (direct db.query() in routers) and new code (repositories) coexist. Migration is incremental:

  1. New endpoints use repositories
  2. Existing endpoints are migrated one at a time
  3. Both access the same DB, same session, same tables

Key Conventions

  • Enums: canonical source is domain/enums.py, models/enums.py re-exports
  • Exceptions: raise from domain/errors.py, never raise HTTPException from services
  • Commits: only via UnitOfWork.commit() or at the router level, never inside services/repos
  • IDs: UUID everywhere (primary keys, foreign keys)
  • Tests: SQLite in-memory for unit/integration, PostgreSQL in CI
  • Validation: Pydantic in schemas (presentation), domain rules in entities (domain)