surfaces/.planning/codebase/CONVENTIONS.md
Mikhail Putilovskij c9072d51ea docs: add codebase map to .planning/codebase/
7 documents covering stack, integrations, architecture, structure,
conventions, testing, and concerns.
2026-04-02 00:00:51 +03:00

195 lines
6.9 KiB
Markdown

# Coding Conventions
**Analysis Date:** 2026-04-01
## Linting and Formatting
**Tool:** ruff (configured in `pyproject.toml`)
**Settings:**
- Line length: 100 characters
- Target: Python 3.11
- Active rule sets: `E` (pycodestyle errors), `F` (pyflakes), `I` (isort), `UP` (pyupgrade), `B` (bugbear)
**Type checking:** mypy (available as dev dependency; not enforced in CI at this time)
Run linting:
```bash
ruff check .
ruff format .
```
## File Naming
- Module files: `snake_case.py` (e.g., `room_router.py`, `test_dispatcher.py`)
- Each module starts with a comment declaring its path: `# core/handler.py`
- Test files: `test_<module>.py` (e.g., `test_store.py`, `test_converter.py`)
- No index/barrel files except `__init__.py` for package registration
## Class Naming
- `PascalCase` for all classes (e.g., `EventDispatcher`, `MockPlatformClient`, `MatrixBot`)
- Protocol/interface classes named after the capability: `StateStore`, `PlatformClient`, `WebhookReceiver`
- Manager classes suffixed with `Manager`: `ChatManager`, `AuthManager`, `SettingsManager`
- Dataclasses follow the same `PascalCase` rule: `IncomingMessage`, `OutgoingUI`, `MatrixRuntime`
## Function and Method Naming
- `snake_case` for all functions and methods
- Private helpers prefixed with single underscore: `_to_dict`, `_from_dict`, `_routing_key`, `_latency`
- Handler functions named `handle_<action>`: `handle_start`, `handle_message`, `handle_new_chat`
- Builder functions named `build_<thing>`: `build_runtime`, `build_event_dispatcher`, `build_skills_text`
- Converter functions named `from_<source>`: `from_room_event`, `from_command`, `from_reaction`
- Predicate functions named `is_<state>`: `is_authenticated`, `is_new`
## Variable Naming
- `snake_case` for all variables and parameters
- Internal state attributes prefixed with `_`: `self._store`, `self._platform`, `self._handlers`
- Store key prefixes are module-level constants in `UPPER_SNAKE_CASE`:
```python
ROOM_META_PREFIX = "matrix_room:"
USER_META_PREFIX = "matrix_user:"
```
- Constants for reaction strings are module-level: `CONFIRM_REACTION = "👍"`, `PLATFORM = "matrix"`
## Type Annotations
All files use `from __future__ import annotations` at the top for deferred evaluation.
**Annotation style:**
- Use built-in generics (`list[str]`, `dict[str, Any]`) — not `List`, `Dict` from `typing`
- Union types written with `|`: `str | None`, `IncomingCallback | None`
- Type aliases at module level: `IncomingEvent = IncomingMessage | IncomingCommand | IncomingCallback`
- Callable types use `typing.Callable` and `typing.Awaitable`:
```python
HandlerFn = Callable[..., Awaitable[list[OutgoingEvent]]]
```
- Handler functions use loose `list` return type without generics (consistent across `core/handlers/`)
- Protocol classes use `...` as body for abstract methods:
```python
async def get(self, key: str) -> dict | None: ...
```
**Pydantic vs dataclasses:**
- `core/protocol.py` — plain `@dataclass` with `field(default_factory=...)` for mutable defaults
- `sdk/interface.py` — Pydantic `BaseModel` for all SDK-facing models (`User`, `MessageResponse`, `UserSettings`)
- Choose `@dataclass` for internal protocol structs, `BaseModel` for SDK boundary models
## Import Organization
Order (enforced by ruff `I` rules):
1. `from __future__ import annotations`
2. Standard library imports (grouped)
3. Third-party imports (grouped)
4. Local imports from project packages (grouped)
Example from `adapter/matrix/bot.py`:
```python
from __future__ import annotations
import asyncio
import os
from dataclasses import dataclass
from pathlib import Path
import structlog
from nio import AsyncClient, ...
from dotenv import load_dotenv
from adapter.matrix.converter import from_reaction, from_room_event
from core.auth import AuthManager
from core.protocol import OutgoingEvent, ...
from sdk.mock import MockPlatformClient
```
No relative imports; all imports use absolute package paths from the project root.
## Async Patterns
All I/O methods are `async def`. There are no sync wrappers around async code.
**Handler signature pattern** (used uniformly across `core/handlers/`):
```python
async def handle_<action>(event: IncomingEvent, auth_mgr, platform, chat_mgr, settings_mgr) -> list:
```
Note: manager parameters are untyped in handler signatures (accepted as `**kwargs` at call site in `EventDispatcher.dispatch`).
**Awaiting store calls:**
```python
stored = await self._store.get(f"auth:{user_id}")
await self._store.set(f"auth:{user_id}", _to_dict(flow))
```
**SQLiteStore uses sync sqlite3** inside `async def` methods — blocking I/O is not off-loaded to a thread executor. This is a known limitation (see CONCERNS.md).
**Mock latency simulation:**
```python
await self._latency(200, 600) # min_ms, max_ms
```
## Logging
**Library:** `structlog`
**Pattern:**
```python
import structlog
logger = structlog.get_logger(__name__)
logger.info("Chat created", chat_id=chat_id, user_id=user_id)
logger.warning("No handler registered", event_type=event_type.__name__, key=key)
```
- Always pass structured keyword arguments — never use f-strings in log calls
- Logger created at module level with `structlog.get_logger(__name__)`
## Error Handling
- Raise `ValueError` for invalid domain state (e.g., chat not found in `ChatManager.rename`)
- `sdk/interface.py` defines `PlatformError(Exception)` with a `code` field for SDK-level errors
- Handler functions never raise — they return `[]` or a fallback `OutgoingMessage`
- No `try/except` blocks in core handlers; errors from the platform are expected to propagate
## Comments
- Module-level comment declaring file path at top: `# core/handler.py`
- Docstrings for classes with non-obvious behavior:
```python
class MockPlatformClient:
"""
Заглушка SDK платформы Lambda.
...
"""
```
- Inline comments for non-obvious blocks:
```python
# Scan by chat_id suffix when user_id unknown (slower)
```
- Comments in Russian are normal and acceptable throughout the codebase
## Serialization Pattern
Dataclasses are serialized/deserialized via private module-level functions, not class methods:
```python
def _to_dict(ctx: ChatContext) -> dict:
return { "chat_id": ctx.chat_id, ... }
def _from_dict(d: dict) -> ChatContext:
return ChatContext(chat_id=d["chat_id"], ...)
```
This pattern is used in `core/auth.py` and `core/chat.py`. Follow this pattern for any new manager that persists to `StateStore`.
## Module Design
- No barrel `__init__.py` exports except `core/handlers/__init__.py` which exposes `register_all`
- Manager classes take `(platform, store)` as constructor args; `platform` is often stored as `object` or not stored at all if unused
- `@dataclass` is preferred for plain data containers, not NamedTuple or TypedDict
- Store key namespacing follows `<namespace>:<user_id>:<entity_id>` pattern:
`"chat:u1:C1"`, `"auth:u1"`, `"matrix_room:!r:m.org"`
---
*Convention analysis: 2026-04-01*