docs: map codebase
This commit is contained in:
parent
6dde5be17d
commit
65445f516f
7 changed files with 73 additions and 1252 deletions
|
|
@ -1,195 +1,7 @@
|
|||
# Coding Conventions
|
||||
# Конвенции (CONVENTIONS.md)
|
||||
|
||||
**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*
|
||||
- **Асинхронность**: Весь код бота асинхронный (`asyncio`). Вызовы SDK и Matrix-клиента выполняются через `await`. Блокирующие вызовы (если они есть) должны выноситься в тредпул.
|
||||
- **Обработка ошибок**: Бот не должен падать из-за ошибок отдельного агента. Ошибки SDK (например, `PlatformError`) отлавливаются в боте и возвращаются пользователю в виде системных сообщений или уведомлений.
|
||||
- **Стейтлесс-подход**: Поверхность хранит минимальный стейт (только локальный SQLite для связки `room_id` <-> `platform_chat_id`). Вся история сообщений и память лежат на стороне агентов.
|
||||
- **Переменные окружения**: Бот полностью конфигурируется через `.env` (префиксы `MATRIX_` и `SURFACES_`).
|
||||
- **Добавление новой поверхности**: Новая поверхность должна быть самостоятельной папкой в `adapter/`, реализовывать `converter.py`, и переиспользовать `sdk/real.py` и `core/protocol.py`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue