# Quality Guidelines > Code quality standards for backend development. --- ## Overview This project enforces quality through: - **Linting**: `ruff` (fast Python linter) - **Type checking**: `basedpyright` (strict type checker) - **Formatting**: Consistent code style via linter rules - **Testing**: `pytest` with async support (`pytest-asyncio`) - **Pre-commit hooks**: Automated checks before commits --- ## Forbidden Patterns ### ❌ Bypassing Lint/Type Gates ```python # WRONG: Suppress linter warnings result = some_function() # noqa: BLE001, reportImplicitRelativeImport ``` **Right way:** Fix the underlying issue: ```python # Fix exception handling except Exception as exc: logger.error("operation_failed", error=str(exc)) raise ``` ### ❌ Using `print()` in Runtime Code ```python # WRONG: Never use print in production code print(f"Processing user {user_id}") ``` **Right way:** Use `core.logging`: ```python from core.logging import get_logger logger = get_logger(__name__) logger.info("processing_user", user_id=user_id) ``` ### ❌ Hardcoding Secrets ```python # WRONG: Hardcode credentials api_key = "sk-abcd1234..." password = "postgres123" ``` **Right way:** Use `core.config.settings`: ```python from core.config.settings import config api_key = config.llm.provider_keys["openai"] password = config.database.password ``` ### ❌ Manual Environment Parsing ```python # WRONG: Direct os.getenv import os db_host = os.getenv("DATABASE_HOST", "localhost") ``` **Right way:** Use `Settings`: ```python from core.config.settings import config db_host = config.database.host ``` ### ❌ Silent Exception Handling ```python # WRONG: Swallow exception try: await service.do_something() except Exception: pass # Destroys debuggability ``` **Right way:** Log and propagate: ```python try: await service.do_something() except Exception as exc: logger.error("operation_failed", error=str(exc), exc_info=True) raise ``` --- ## Required Patterns ### ✅ Strong Typing at Boundaries ```python from pydantic import BaseModel # Request/response schemas must use Pydantic class CreateSessionRequest(BaseModel): thread_id: str messages: list[Message] class SessionResponse(BaseModel): id: UUID owner_id: UUID created_at: datetime ``` ### ✅ Schema-First for Data Contracts ```python # Define schema first, then implement # v1/agent/schemas.py class TaskAccepted(BaseModel): run_id: str thread_id: str accepted_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) # Then use in router @router.post("/run", response_model=TaskAccepted) async def create_run(...): return TaskAccepted(run_id=run_id, thread_id=thread_id) ``` ### ✅ Dependency Injection via FastAPI ```python # v1/agent/dependencies.py from fastapi import Depends from core.db.base_repository import BaseRepository async def get_agent_service( session: AsyncSession = Depends(get_session), ) -> AgentService: repository = AgentRepository(session) return AgentService(repository=repository, ...) # v1/agent/router.py @router.post("/run") async def create_run( service: AgentService = Depends(get_agent_service), ): return await service.enqueue_run(...) ``` ### ✅ Configuration via Settings ```python # All config through Settings from core.config.settings import config class SomeService: def __init__(self): self.redis_url = config.redis.url self.db_url = config.database.url self.log_level = config.runtime.log_level ``` ### ✅ Logging with Context ```python # Always include relevant context logger.info( "run_completed", run_id=run_id, thread_id=thread_id, duration_seconds=duration, user_id=current_user.id, ) ``` --- ## Testing Requirements ### Test Organization ``` backend/tests/ ├── unit/ # Unit tests (no external dependencies) ├── integration/ # Integration tests (DB, Redis, HTTP) ├── conftest.py # Shared fixtures └── ... ``` ### Test Requirements **From AGENTS.md:** 1. **TDD when practical**: Write tests before/alongside implementation 2. **Regression tests**: Changed logic/contracts must have regression tests 3. **Real DB tests**: Use `settings.test.*` credentials (never hardcode) 4. **Integration tests**: Start backend via `./infra/scripts/app.sh` before running 5. **Restart for integration**: Use `./infra/scripts/app.sh restart` before `uv run pytest` ### Test Configuration ```python # pyproject.toml [tool.pytest.ini_options] testpaths = ["backend/tests"] addopts = "-q --import-mode=importlib" asyncio_mode = "auto" pythonpath = ["backend/src"] ``` ### Example Test ```python import pytest from httpx import AsyncClient from core.config.settings import Settings @pytest.mark.asyncio async def test_create_session(settings: Settings): # Use settings.test.* for real DB tests assert settings.test.phone != "" async with AsyncClient() as client: response = await client.post( f"{settings.supabase.url}/api/v1/agent/run", json={"thread_id": "test-thread"}, ) assert response.status_code == 200 ``` --- ## Code Review Checklist ### Architecture - [ ] Follows `schema -> repository -> service` layering - [ ] Router handles HTTP only, service handles business logic - [ ] Repository does not make auth decisions - [ ] Transaction boundary is in service layer, not repository ### Security - [ ] `owner_id` comes from `current_user.id` (JWT sub claim), not client payload - [ ] No hardcoded secrets/passwords/tokens - [ ] Sensitive fields are not logged (passwords, tokens, PII) - [ ] Auth checks use `ApiProblemError` with proper error codes ### Data - [ ] Database changes use Alembic migrations - [ ] Soft delete uses `deleted_at`, queries filter deleted records - [ ] Timezone-aware timestamps: `datetime.now(timezone.utc)` - [ ] Foreign keys follow `_id` naming ### Error Handling - [ ] All exceptions are logged before re-raising - [ ] Business logic uses `ApiProblemError`, not `HTTPException` - [ ] Error responses use RFC 7807 format with `code` field - [ ] New error codes documented in `docs/protocols/common/http-error-codes.md` ### Logging - [ ] Uses `core.logging.get_logger`, not `print()` - [ ] Log level appropriate for event (ERROR for failures, INFO for milestones) - [ ] Context includes relevant identifiers (run_id, user_id, thread_id) - [ ] No sensitive data in logs ### Testing - [ ] Unit tests for service logic - [ ] Integration tests for changed contracts - [ ] Test credentials via `settings.test.*` - [ ] Regression tests for bug fixes ### Code Style - [ ] Passes `ruff` linting - [ ] Passes `basedpyright` type checking - [ ] Follows naming conventions (snake_case for functions/variables) - [ ] No broad `except Exception:` without logging/re-raising --- ## Linting & Type Checking ### Ruff (Linter) **Run:** ```bash uv run ruff check backend/src/ ``` **Common fixes:** - Remove unused imports - Fix line length (max 100 chars) - Use `async def` for async functions - Add type annotations ### Basedpyright (Type Checker) **Run:** ```bash uv run basedpyright backend/src/ ``` **Common fixes:** - Add type hints for function parameters - Use `Optional[T]` for optional values - Fix `reportImplicitRelativeImport` warnings - Handle `None` cases explicitly ### Pre-commit Hooks **Setup:** ```bash uv run pre-commit install ``` **Runs automatically on:** - `ruff check` - `basedpyright` - Formatting checks --- ## Common Mistakes ### ❌ Missing Type Annotations ```python # WRONG: No type hints def get_user(user_id): return repository.get_by_id(user_id) ``` **Right:** ```python async def get_user(user_id: UUID) -> User | None: return await repository.get_by_id(user_id) ``` ### ❌ Catch-All Exception Handling ```python # WRONG: Catching all exceptions without logging except Exception: return None ``` **Right:** ```python except SomeSpecificError as exc: logger.error("operation_failed", error=str(exc), exc_info=True) raise ApiProblemError(...) from exc ``` ### ❌ Timezone-Naive Datetimes ```python # WRONG: No timezone created_at = datetime.now() ``` **Right:** ```python created_at = datetime.now(timezone.utc) ``` ### ❌ Testing with Hardcoded Credentials ```python # WRONG: Hardcoded test DB DATABASE_URL = "postgres://user:pass@localhost:5432/test" ``` **Right:** ```python from core.config.settings import config # Use settings.test.* assert config.database.host is not None ```