Skip to content

Code Standards

This document outlines the coding standards and conventions used in Cognitive Companion. Following these ensures consistency and helps AI coding agents work effectively with the codebase.

Python (Backend)

Style

  • Linter/Formatter: ruff with E, F, I, W, UP, B, SIM, RUF, PIE, PT, C4, T20 rules
  • Type checker: mypy with enable_error_code = ["import"] (catches broken imports)
  • Line length: 100 characters
  • Target: Python 3.14
  • Async: Use async/await for all I/O operations

Foundation Layer: backend.core.*

The backend/core/ package holds a higher bar than the rest of the tree and is the reference implementation new contributors should read first. A dedicated [[tool.mypy.overrides]] section in backend/pyproject.toml applies disallow_untyped_defs = true and disallow_incomplete_defs = true to backend.core.* only; the rest of the tree remains on gradual adoption.

Rules specific to code under backend/core/:

  • Every function must carry full type annotations.
  • No imports from higher-level packages (backend.services, backend.routers, backend.channels, backend.steps, etc.). backend.models may only be imported lazily inside Database.create_all.
  • Framework imports are allowed only at FastAPI-facing leaves: auth.py and exceptions.register_exception_handlers.
  • Every stateful module-level singleton must be a thin facade over a class (Settings, Database, KeyStore) that can be constructed in a test without touching process globals.
  • Public API changes require a corresponding update to backend/tests/core/.

The canonical test suite for this layer lives at backend/tests/core/ and covers over 200 test cases across multiple modules. Treat it as the contract: if a refactor changes a public behavior in backend.core.*, those tests should be updated in the same commit.

Running the developer gates

bash
make test              # full backend suite
make test-core         # backend.core only
make test-services     # backend.services only
make coverage          # backend.core with branch coverage (terminal)
make coverage-services # backend.services with branch coverage
make coverage-html     # + HTML report under ./htmlcov
make lint              # ruff (no fixes)
make lint-fix          # ruff with --fix
make format            # ruff format
make typecheck         # mypy over the full backend tree
make typecheck-core    # strict mypy over backend.core only
make check             # lint + typecheck-core + test-core (fast gate)
make check-all         # lint + typecheck-core + test-core + test-services

Imports

All imports must be at the top of the file, following PEP 8:

python
# Standard library
import json
from datetime import datetime, timezone
from pathlib import Path

# Third-party
from fastapi import APIRouter, Depends, Request
from sqlalchemy import select
from sqlalchemy.orm import Session
from PIL import Image

# Local
from backend.core.config import settings
from backend.core.logging import get_logger

Exception: Optional dependencies (e.g., google-genai) may use guarded lazy imports with a comment explaining why:

python
# In the function that needs it:
from google import genai  # Lazy import: google-genai is an optional dependency

Logging

Use get_logger() from backend.core.logging. Never use print(). The logger wraps Python's stdlib logging module and accepts keyword context arguments that are appended to the log line as key=value pairs:

python
from backend.core.logging import get_logger
logger = get_logger(__name__)

logger.info("event_processed", sensor_id=sid, rule=rule.name)
# → "event_processed sensor_id=cam1 rule=Motion Alert"

logger.error("pipeline_step_failed", step_type=step.step_type, error=str(e))
logger.exception("flush_db_error", sensor_id=sensor_id)  # includes traceback

Error Handling

Raise custom exceptions from backend/core/exceptions.py:

ExceptionHTTP Status
AuthenticationError401
PermissionDeniedError403
NotFoundError404
ConflictError409

Do not catch these in routers. Let the global exception handlers convert them to HTTP responses.

In backend service code, always log errors:

python
try:
    result = await some_operation()
except SomeError as e:
    logger.error("operation_failed", error=str(e))
    raise

Service Pattern

Services are instantiated in the FastAPI lifespan and attached to app.state:

python
# In backend/main.py (lifespan):
app.state.my_service = MyService(dependencies...)

# In a router:
my_service = request.app.state.my_service

Do not instantiate services inside routers or import them at module level in router files.

Configuration

Access config values via dot-notation:

python
from backend.core.config import settings

url = settings.get("person_id.url")
interval = settings.get("homeassistant.poll_interval_seconds", 30)

Store secrets in environment variables, referenced in YAML via ${ENV_VAR} syntax. Never hardcode secrets.

JavaScript/Vue (Frontend)

Framework

  • Vue 3 with Composition API (<script setup>)
  • Vuetify 3 for Material Design components
  • Pinia for state management
  • Vite for build tooling

User Feedback

Use composables instead of browser dialogs:

javascript
// Instead of alert() / confirm():
import { useNotify } from "@/composables/useNotify";
import { useConfirm } from "@/composables/useConfirm";

const { notify } = useNotify();
const { showConfirm } = useConfirm();

// Success notification
notify("Item saved successfully");

// Error notification
notify("Failed to save: " + error.message, "error");

// Confirmation dialog
const confirmed = await showConfirm("Delete Item", "Are you sure?");
if (confirmed) {
  await deleteItem();
}

Error Handling

Never swallow errors silently. Bare catch {} blocks must log:

javascript
// Bad
try { await fetchData(); } catch {}

// Good
try {
  await fetchData();
} catch (e) {
  console.error("Failed to load data:", e);
  items.value = [];
}

Component Cleanup

Always clean up event listeners and intervals:

javascript
import { onMounted, onUnmounted } from "vue";

onMounted(() => {
  window.addEventListener("resize", handleResize);
});

onUnmounted(() => {
  window.removeEventListener("resize", handleResize);
});

CTS-specific standards

CTS code under backend/services/cts/ and backend/routers/cts*.py follows additional rules:

Shared utilities

Three files are the single authoritative source for functions that were historically duplicated across the codebase:

UtilityImport fromReplaces
ns_to_iso()backend.services.cts._time4 copies across subscriber files
parse_ts()backend.services.cts._time3 divergent copies across service files
ensure_aware()backend.services.cts._time1 copy in source_authority
cts_enabled()backend.routers.cts_deps8 copies across router files

Import these; never redefine them.

Protocol-based dependency injection

CTS subscribers and services use structural Protocol classes from backend/services.cts._types instead of Any:

  • ConnectionManager: WebSocket broadcast and disconnect
  • PipelineExecutor: event firing interface
  • MinioClient: presigned URL generation and object retrieval
  • SceneAnalysisClient: YOLO, Florence-2, CLIP analysis
  • SemanticMemoryClient: observation creation and queries
  • DBSessionFactory: Callable[[], Session] type alias for db_factory parameters

StreamConsumer contract

All four CTS subscribers extend StreamConsumer[T]. The decode parameter uses dict[bytes | str, bytes | str] because Redis returns bytes when decode_responses=False. Messages are protobuf-encoded; the compiled bindings live in backend/integrations/proto/continuoustracking/v1/.

Extraction threshold

When the same logic appears in three or more places, extract it to a shared module. For CTS, this rule produced _time.py, cts_deps.py, and _types.py. For the frontend, it produced useCtsSeverity.js, useFormatRelative.js, and useCtsWebSocket.js.

No-silent-fallback rule

Every failure path must be explicit. This rule is enforced by the BLE001 (blind-except) ruff rule at error severity in both CTS and CC. An allowlist of permitted catch-all sites is maintained in each repo's pyproject.toml.

ForbiddenRequired instead
except Exception: passLog and return a typed error or raise AppError
.get("required_key", default_value) for a required fieldDirect dict access or Pydantic validation; raise on missing
try-old-then-new fallback shimsExplicit version-aware decoding at the boundary
Returning fabricated zero/empty on missing dataRaise a typed exception or return a documented None

CTS stream consumers that cannot process a message dead-letter it: they XACK the message, log at warning level, and increment a Prometheus counter. The message is never silently skipped.

MCP and BFF parity (D6)

Any data exposed to the Vue UI through a FastAPI router must be exposed to MCP tools by reading the same service function. MCP tools may not contain query logic; they call service methods and adapt the result. Import-linter contracts in pyproject.toml enforce that backend.mcp may not import from backend.models or repository modules directly.

Frontend visualisation rules (D2/D3)

  • One shared component per data shape. Time series, bars, heatmaps, scatter, and DAGs are rendered by the shared ECharts components in components/charts/ and components/process/. No view hand-rolls a chart of the same shape.
  • ECharts via vue-echarts, explicit imports only. No full-bundle ECharts import; no second charting library.
  • Bespoke canvas only for spatial domains. Floor-plan overlays and bounding-box-on-keyframe views may use SVG/Canvas, but they must consume --cc- design tokens and the useChartTheme composable.
  • Quality is a first-class field (D5). Confidence, quality, staleness, and source fields travel from CTS through the BFF envelope to the UI. The UI renders them; it never computes them client-side. Use CcProvenanceBadge to display source information.

Do NOT

  • Run migrations by hand in production. Use Alembic via make migrate.
  • Use print(). Use get_logger() from backend.core.logging.
  • Instantiate services in routers. Access them from request.app.state.
  • Add dependencies without updating pyproject.toml and running uv lock (backend) or package.json (frontend).
  • Skip permission checks. All new endpoints need entries in config/auth.yaml.
  • Catch AuthenticationError or PermissionDeniedError in routers. Let global handlers deal with them.
  • Store secrets in config files. Use ${ENV_VAR} interpolation.
  • Hardcode pipeline step order. Use the PipelineStep model with its order field.
  • Use eval() for condition expressions. Use evaluate_condition() from backend.core.template (Lark-based grammar).
  • Use lazy imports for required dependencies. All imports belong at the top of the file.
  • Use alert() or confirm() in Vue views. Use composables.
  • Swallow errors silently. Bare catch {} blocks must log.
  • Duplicate _cts_enabled() in CTS router files. Import from backend.routers.cts_deps.
  • Duplicate ns_to_iso() or parse_ts() in CTS service files. Import from backend.services.cts._time.
  • Use Any for injected service parameters in CTS code. Use protocol types from backend.services.cts._types.
  • Hardcode hex color values in Vue templates. Use var(--cc-*) design tokens or Vuetify theme colors.
  • Duplicate severityColor or formatRelative in Vue views. Import from frontend/src/composables/.

Released under the AGPL-3.0 License.