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

6.9 KiB

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:

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:
    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:
    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:
    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:

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/):

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:

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:

await self._latency(200, 600)  # min_ms, max_ms

Logging

Library: structlog

Pattern:

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:
    class MockPlatformClient:
        """
        Заглушка SDK платформы Lambda.
        ...
        """
    
  • Inline comments for non-obvious blocks:
    # 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:

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