Files
Mai/.planning/codebase/CONVENTIONS.md
Mai Development f238a958a0 docs: map existing codebase
- STACK.md - Technologies and dependencies
- ARCHITECTURE.md - System design and patterns
- STRUCTURE.md - Directory layout
- CONVENTIONS.md - Code style and patterns
- TESTING.md - Test structure
- INTEGRATIONS.md - External services
- CONCERNS.md - Technical debt and issues
2026-01-26 23:14:44 -05:00

299 lines
8.7 KiB
Markdown

# Coding Conventions
**Analysis Date:** 2026-01-26
## Status
**Note:** This codebase is in planning phase. No source code has been written yet. These conventions are **prescriptive** for the Mai project and should be applied to all code from the first commit forward.
## Naming Patterns
**Files:**
- Python modules: `lowercase_with_underscores.py` (PEP 8)
- Configuration files: `config.yaml`, `.env.example`
- Test files: `test_module_name.py` (co-located with source)
- Example: `src/memory/storage.py`, `src/memory/test_storage.py`
**Functions:**
- Use `snake_case` for all function names (PEP 8)
- Private functions: Prefix with single underscore `_private_function()`
- Async functions: Use `async def async_operation()` naming
- Example: `def get_conversation_history()`, `async def stream_response()`
**Variables:**
- Use `snake_case` for all variable names
- Constants: `UPPERCASE_WITH_UNDERSCORES`
- Private module variables: Prefix with `_`
- Example: `conversation_history`, `MAX_CONTEXT_TOKENS`, `_internal_cache`
**Types:**
- Classes: `PascalCase`
- Enums: `PascalCase` (inherit from `Enum`)
- TypedDict: `PascalCase` with `Dict` suffix
- Example: `class ConversationManager`, `class ErrorLevel(Enum)`, `class MemoryConfigDict(TypedDict)`
**Directories:**
- Core modules: `src/[module_name]/` (lowercase, plural when appropriate)
- Example: `src/models/`, `src/memory/`, `src/safety/`, `src/interfaces/`
## Code Style
**Formatting:**
- Tool: **Ruff** (formatter and linter)
- Line length: 88 characters (Ruff default)
- Quote style: Double quotes (`"string"`)
- Indentation: 4 spaces (no tabs)
**Linting:**
- Tool: **Ruff**
- Configuration enforced via `.ruff.toml` (when created)
- All imports must pass ruff checks
- No unused imports allowed
- Type hints required for public functions
**Python Version:**
- Minimum: Python 3.10+
- Use modern type hints: `from typing import *`
- Use `str | None` instead of `Optional[str]` (union syntax)
## Import Organization
**Order:**
1. Standard library imports (`import os`, `import sys`)
2. Third-party imports (`import discord`, `import numpy`)
3. Local imports (`from src.memory import Storage`)
4. Blank line between each group
**Example:**
```python
import asyncio
import json
from pathlib import Path
from typing import Optional
import discord
from dotenv import load_dotenv
from src.memory import ConversationStorage
from src.models import ModelManager
```
**Path Aliases:**
- Use relative imports from `src/` root
- Avoid deep relative imports (no `../../../`)
- Example: `from src.safety import SandboxExecutor` not `from ...safety import SandboxExecutor`
## Error Handling
**Patterns:**
- Define domain-specific exceptions in `src/exceptions.py`
- Use exception hierarchy (base `MaiException`, specific subclasses)
- Always include context in exceptions (error code, details, suggestions)
- Example:
```python
class MaiException(Exception):
"""Base exception for Mai framework."""
def __init__(self, code: str, message: str, details: dict | None = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(f"[{code}] {message}")
class ModelError(MaiException):
"""Raised when model inference fails."""
pass
class MemoryError(MaiException):
"""Raised when memory operations fail."""
pass
```
- Log before raising (see Logging section)
- Use context managers for cleanup (async context managers for async code)
- Never catch bare `Exception` - catch specific exceptions
## Logging
**Framework:** `logging` module (Python standard library)
**Patterns:**
- Create logger per module: `logger = logging.getLogger(__name__)`
- Log levels guide:
- `DEBUG`: Detailed diagnostic info (token counts, decision trees)
- `INFO`: Significant operational events (conversation started, model loaded)
- `WARNING`: Unexpected but handled conditions (fallback triggered, retry)
- `ERROR`: Failed operation (model error, memory access failed)
- `CRITICAL`: System-level failures (cannot recover)
- Structured logging preferred (include operation context)
- Example:
```python
import logging
logger = logging.getLogger(__name__)
async def invoke_model(prompt: str, model: str) -> str:
logger.debug(f"Invoking model={model} with token_count={len(prompt.split())}")
try:
response = await model_manager.generate(prompt)
logger.info(f"Model response generated, length={len(response)}")
return response
except ModelError as e:
logger.error(f"Model invocation failed: {e.code}", exc_info=True)
raise
```
## Comments
**When to Comment:**
- Complex logic requiring explanation (multi-step algorithms, non-obvious decisions)
- Important context that code alone cannot convey (why a workaround exists)
- Do NOT comment obvious code (`x = 1 # set x to 1` is noise)
- Do NOT duplicate what the code already says
**JSDoc/Docstrings:**
- Use Google-style docstrings for all public functions/classes
- Include return type even if type hints exist (for readability)
- Example:
```python
async def get_memory_context(
query: str,
max_tokens: int = 2000,
) -> str:
"""Retrieve relevant memory context for a query.
Performs vector similarity search on conversation history,
compresses results to fit token budget, and returns formatted context.
Args:
query: The search query for memory retrieval.
max_tokens: Maximum tokens in returned context (default 2000).
Returns:
Formatted memory context as markdown-structured string.
Raises:
MemoryError: If database query fails or storage is corrupted.
"""
```
## Function Design
**Size:**
- Target: Functions under 50 lines (hard limit: 100 lines)
- Break complex logic into smaller helper functions
- One responsibility per function (single responsibility principle)
**Parameters:**
- Maximum 4 positional parameters
- Use keyword-only arguments for optional params: `def func(required, *, optional=None)`
- Use dataclasses or TypedDict for complex parameter groups
- Example:
```python
# Good: Clear structure
async def approve_change(
change_id: str,
*,
reviewer_id: str,
decision: Literal["approve", "reject"],
reason: str | None = None,
) -> None:
pass
# Bad: Too many params
async def approve_change(change_id, reviewer_id, decision, reason, timestamp, context, metadata):
pass
```
**Return Values:**
- Explicitly return values (no implicit `None` returns unless documented)
- Use `Optional[T]` or `T | None` in type hints for nullable returns
- Prefer returning data objects over tuples: return `Result` not `(status, data, error)`
- Async functions return awaitable, not callbacks
## Module Design
**Exports:**
- Define `__all__` in each module to be explicit about public API
- Example in `src/memory/__init__.py`:
```python
from src.memory.storage import ConversationStorage
from src.memory.compression import MemoryCompressor
__all__ = ["ConversationStorage", "MemoryCompressor"]
```
**Barrel Files:**
- Use `__init__.py` to export key classes/functions from submodules
- Keep import chains shallow (max 2 levels deep)
- Example structure:
```
src/
├── memory/
│ ├── __init__.py (exports Storage, Compressor)
│ ├── storage.py
│ └── compression.py
```
**Async/Await:**
- All I/O operations (database, API calls, file I/O) must be async
- Use `asyncio` for concurrency, not threading
- Async context managers for resource management:
```python
async def process_request(prompt: str) -> str:
async with model_manager.get_session() as session:
response = await session.generate(prompt)
return response
```
## Type Hints
**Requirements:**
- All public function signatures must have type hints
- Use `from __future__ import annotations` for forward references
- Prefer union syntax: `str | None` over `Optional[str]`
- Use `Literal` for string enums: `Literal["approve", "reject"]`
- Example:
```python
from __future__ import annotations
from typing import Literal
def evaluate_risk(code: str) -> Literal["LOW", "MEDIUM", "HIGH", "BLOCKED"]:
"""Evaluate code risk level."""
pass
```
## Configuration
**Pattern:**
- Use YAML for human-editable config files
- Use environment variables for secrets (never commit `.env`)
- Validation at import time (fail fast if config invalid)
- Example:
```python
# config.py
import os
from pathlib import Path
class Config:
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
MODELS_PATH = Path(os.getenv("MODELS_PATH", "~/.mai/models")).expanduser()
MAX_CONTEXT_TOKENS = int(os.getenv("MAX_CONTEXT_TOKENS", "8000"))
# Validate on import
if not MODELS_PATH.exists():
raise RuntimeError(f"Models path does not exist: {MODELS_PATH}")
```
---
*Convention guide: 2026-01-26*
*Status: Prescriptive for Mai v1 implementation*