# Coding Conventions

**Analysis Date:** 2026-04-01

## Naming Patterns

**Files:**
- `snake_case.py` throughout — `coordinates.py`, `chunk_manager.py`, `processor.py`
- Test files: `test_<module>.py` prefix — `test_coordinates.py`, `test_api_flights.py`
- Schema files grouped by domain — `flight.py`, `vo.py`, `graph.py`, `metric.py`

**Classes:**
- `PascalCase` for all classes — `FlightProcessor`, `CoordinateTransformer`, `FactorGraphOptimizer`
- Interface/ABC classes prefixed with `I` — `IImageMatcher`, `ISequentialVisualOdometry`, `IModelManager`
- Error/Exception classes suffixed with `Error` — `OriginNotSetError`, `QueueFullError`, `ValidationError`
- Pydantic schema classes named after the concept they represent, suffixed with `Request`/`Response` for API boundaries — `FlightCreateRequest`, `FlightDetailResponse`, `BatchUpdateResponse`
- Config classes suffixed with `Config` or `Settings` — `DatabaseConfig`, `RecoveryConfig`, `AppSettings`

**Functions/Methods:**
- `snake_case` — `set_enu_origin`, `compute_relative_pose`, `retrieve_candidate_tiles`
- Async functions not distinguished by name from sync (no `async_` prefix); the `async def` keyword is the signal
- Private/internal methods prefixed with single underscore — `_init_flight`, `_cleanup_flight`, `_publish_frame_result`

**Variables:**
- `snake_case` — `flight_id`, `rel_pose`, `chunk_mgr`
- Per-flight in-memory dicts named `_<plural_noun>` keyed by `flight_id` — `_origins`, `_flight_states`, `_prev_images`, `_flight_cameras`
- Constants: uppercase not consistently enforced; numeric magic values appear in comments (e.g., `111319.5`)

**Type Parameters:**
- `PascalCase` — standard Python typing conventions

## Code Style

**Formatter/Linter:**
- `ruff` — configured in `pyproject.toml`
- Line length: 100 characters (`line-length = 100`)
- Target: Python 3.11 (`target-version = "py311"`)
- Ruff rule sets active: `E` (pycodestyle errors), `F` (Pyflakes), `I` (isort), `W` (pycodestyle warnings)
- No `B` (flake8-bugbear) or `UP` (pyupgrade) rules enabled

**Type Hints:**
- Type hints on all public method signatures — parameters and return types
- `from __future__ import annotations` used in modules with complex forward references (`processor.py`, `flight.py`, `config.py`)
- `Optional[T]` used alongside `T | None` syntax (inconsistency — both styles present)
- `dict[str, X]` and `list[X]` lowercase generics (Python 3.9+ style)
- Untyped component slots use `= None` without annotation — e.g., `self._vo = None` — intentional for lazy init

## Module Docstrings

Every source module starts with a one-line docstring naming the component:
```python
"""Coordinate Transformer (Component F13)."""
"""Core Flight Processor — Full Processing Pipeline (Stage 10)."""
"""Sequential Visual Odometry (Component F07)."""
```
Test files also carry docstrings stating what they test and which component ID they cover.

## Import Organization

**Order (enforced by ruff `I` rules):**
1. Standard library (`__future__`, `asyncio`, `logging`, `math`, `abc`)
2. Third-party (`fastapi`, `numpy`, `cv2`, `pydantic`, `sqlalchemy`)
3. Internal project (`gps_denied.*`)

**Internal imports:**
- Always absolute — `from gps_denied.core.coordinates import CoordinateTransformer`
- Schema imports collected into explicit multi-line blocks at top of file
- Lazy in-function imports used to avoid circular imports: `from gps_denied.schemas import Geofences` inside a method body in `processor.py`

## ABC/Interface Pattern

All major processing components define an Abstract Base Class in the same module:
```python
class ISequentialVisualOdometry(ABC):
    @abstractmethod
    def compute_relative_pose(...) -> RelativePose | None: ...

class SequentialVisualOdometry(ISequentialVisualOdometry):
    ...
```
Components that depend on another component accept the interface type in `__init__`, enabling mock injection:
```python
class SequentialVisualOdometry(ISequentialVisualOdometry):
    def __init__(self, model_manager: IModelManager): ...
```
This pattern appears in: `vo.py`, `models.py`, `rotation.py`.

`FlightProcessor` uses a post-construction injection method instead:
```python
def attach_components(self, vo=None, gpr=None, metric=None, ...): ...
```

## Pydantic Schema Conventions

- All schemas inherit from `pydantic.BaseModel`
- Validation constraints use `Field(..., ge=..., le=..., gt=..., min_length=..., max_length=...)`
- Default values use `Field(default_factory=...)` for mutable defaults
- `model_dump()` and `model_validate_json()` used (Pydantic v2 API)
- Config classes inherit from `pydantic_settings.BaseSettings` with `SettingsConfigDict(env_prefix=...)`
- Grouped into separate schema files by domain: `flight.py`, `vo.py`, `graph.py`, `metric.py`, `gpr.py`, `chunk.py`, `rotation.py`, `satellite.py`, `events.py`, `image.py`, `model.py`

## Error Handling

**Custom exceptions:**
- Defined in the module where they originate — `OriginNotSetError` in `coordinates.py`, `QueueFullError` / `ValidationError` in `pipeline.py`
- Inherit directly from `Exception` (no base project exception class)

**In async pipeline code:**
- `try/except Exception as exc` with `logger.warning(...)` — swallowed errors are logged, not re-raised:
  ```python
  except Exception as exc:
      logger.warning("VO failed for frame %d: %s", frame_id, exc)
  ```
- HTTP layer raises `HTTPException` from FastAPI — no custom HTTP exception hierarchy

**Type-ignore suppressions:**
- `# type: ignore[union-attr]` used on SQLAlchemy `result.rowcount` accesses in `repository.py`
- `# type: ignore` on two FastAPI dependency parameters in `flights.py` (multipart form endpoint)

## Logging

**Framework:** Python stdlib `logging`

**Setup per module:**
```python
import logging
logger = logging.getLogger(__name__)
```

**Usage patterns:**
- `logger.warning(...)` for recoverable failures (VO failure, drift correction failure)
- `logger.info(...)` for state machine transitions: `"Flight %s → LOST at frame %d"`
- `logger.debug(...)` for high-frequency data: optimization convergence results

## Section Separators

Long modules use visual comment dividers to delineate sections:
```python
# ---------------------------------------------------------------------------
# State Machine
# ---------------------------------------------------------------------------
```
and
```python
# =========================================================
# process_frame — central orchestration
# =========================================================
```

## Function Design

**Size:** Methods tend to be 10-40 lines; `process_frame` is intentionally longer (~80 lines) as the central orchestrator.

**Parameters:** Typed, ordered: `self`, required positional, optional keyword with defaults.

**Return Values:**
- Async methods return domain schema objects or `None` on not-found
- Sync methods return `bool` for success/failure of CRUD operations
- `Optional[T]` / `T | None` used for nullable returns consistently

## Comments

**Inline comments** used liberally to explain non-obvious math or mock logic:
```python
# 111319.5 meters per degree at equator
# FAKE Math for mockup:
# Very rough scaling: assume 1 pixel is ~0.1 meter
```

**Section-level comments** label major logical steps inside long methods:
```python
# ---- 1. Visual Odometry (frame-to-frame) ----
# ---- 2. State Machine transitions ----
```

## Module Design

**Exports:** No explicit `__all__` in any module; consumers import directly from sub-modules.

**Barrel Files:** `src/gps_denied/schemas/__init__.py` re-exports common types (`GPSPoint`, `CameraParameters`, `Geofences`, `Polygon`). Other packages use direct imports.

---

*Convention analysis: 2026-04-01*
