# Coding Conventions

<!-- vibeflow:auto:start -->

## Naming
- **Files**: `snake_case.py`, no prefixes. Modules named by domain concept (`extract.py`, `reconcile.py`, `retrieval.py`).
- **Classes**: `PascalCase`. Dataclasses for results (`WriteResult`, `UpsertResult`, `ConsolidationResult`). Pydantic `BaseModel` for LLM schemas (`FactCandidate`, `ExtractedEntity`).
- **Functions**: `snake_case`. Private helpers prefixed with `_` (e.g., `_call_llm`, `_fetch_entity_facts`).
- **Constants**: `UPPER_SNAKE_CASE` (e.g., `CONFIDENCE_LEVEL_MAP`, `SIMILARITY_THRESHOLD`).
- **Variables**: `snake_case`. Type aliases via `Literal` not `Enum`.

## Code Style
- **Line length**: 120 chars (ruff config).
- **Quotes**: Double quotes everywhere.
- **Imports**: Sorted by ruff (isort). `from __future__ import annotations` used in some modules.
- **Type hints**: Strict mypy (`disallow_untyped_defs=True`). Use `str | None` union syntax (not `Optional`). `list[...]` not `List[...]`.
- **Docstrings**: Google-style with `Args:` and `Returns:` sections. Module-level docstring on every file describing purpose.

## Architecture
- **Dependency injection**: All LLM/embedding calls go through injected `LLMProvider`/`EmbeddingProvider` protocols. No global imports of provider implementations.
- **No inheritance for providers**: Structural subtyping via `typing.Protocol` (duck typing).
- **Config as dataclass**: `MemoryConfig` is a plain `@dataclass` (not Pydantic) with defaults. No env var reading — consumer provides values.
- **Async everywhere**: All DB and LLM operations are async. `AsyncSession` from SQLAlchemy.
- **Lazy imports**: Heavy dependencies imported inside functions (e.g., `from arandu.write.pipeline import run_write_pipeline` inside `client.py`).

## File Organization
- **Package root exports**: `__init__.py` re-exports all public API types. `__all__` sorted alphabetically.
- **`__init__.py` in subpackages**: Re-exports subpackage API (e.g., `background/__init__.py`).
- **Private helpers**: `_helpers.py` or `_` prefixed functions within modules.
- **Prompts**: Inline as module-level string constants (e.g., `EXTRACTION_SYSTEM_PROMPT`), not in separate files.

## Logging
- Module-level `logger = logging.getLogger(__name__)`.
- `logger.info()` for pipeline milestones with structured key=value format: `"write_pipeline: user=%s, event=%s, added=%d"`.
- `logger.warning()` for recoverable errors.
- `logger.debug()` for non-critical diagnostics.

## Error Handling
- Custom exception hierarchy rooted at `MemoryError`.
- Pipeline stages wrapped in try/except with fail-safe defaults (empty results, default ADD).
- Savepoint pattern (`async with session.begin_nested()`) for per-operation DB isolation.
- `__cause__` exception chaining via `cause` parameter.

## Testing
- Tests in `tests/` (flat, not mirroring `src/` structure).
- File naming: `test_<module>.py` (e.g., `test_write_pipeline.py`, `test_clustering.py`).
- Class-based test grouping: `class TestExtract:`, `class TestReconcile:`.
- **No DB required**: All tests use `FakeLLMProvider` and `FakeEmbeddingProvider` from `conftest.py`.
- `run_async()` helper wraps `asyncio.run()` for sync test methods.
- `make_fact()` factory returns `MagicMock(spec=MemoryFact)` with defaults.
- `pytest-asyncio` with `asyncio_mode = "auto"`.

<!-- vibeflow:auto:end -->
