7 documents covering stack, integrations, architecture, structure, conventions, testing, and concerns.
195 lines
6.9 KiB
Markdown
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*
|