# Engineering standards This file is the contract for every contributor to this repository — human or agent. `CLAUDE.md` is a symlink to it: one canonical standard, read by every tool. `ruff`, `mypy`, `eslint`, `prettier`, and `vue-tsc` enforce mechanically what they can; this document carries the rest. ## Architecture Monorepo with a ports-and-adapters seam. The dependency direction is enforced by review: ``` api → pipeline → ports ← adapters domain imports nothing app-level ``` - `backend/app/domain/` — pure models and logic (`Track`, `TasteProfile`, fuzzy matching, profile compression). Imports nothing app-level. - `backend/app/ports/` — `Protocol` definitions (`MusicCatalog`, `Recommender`, `PlaylistWriter`). - `backend/app/adapters/` — implementations: `spotify/` (auth, thin httpx client, DTO mapping), `anthropic/` (both LLM calls), `demo/` (fixture-replay implementations of the same ports). - `backend/app/pipeline/` — the orchestrator composes the stages; stages never import each other. - Spotify JSON never escapes `adapters/spotify/mapping.py`. - Pipeline tunables (counts, thresholds, budgets, model id, effort, TTLs) live in `app/config.py` (pydantic-settings) — no magic numbers in code. - LLM prompts are versioned template files under `app/prompts/`, not inline strings. - `eval/scenarios.yaml` is one source of truth for three consumers: golden eval queries, demo fixture keys, and UI suggestion chips. ## Naming Human-readable, descriptive, full words. Code reads as prose. - Python: `snake_case` functions/variables, `PascalCase` classes, `UPPER_SNAKE` constants. Functions verb-first (`resolve_candidate`), variables noun-first (`grounded_tracks`), booleans as predicates (`is_grounded`, `should_stream`). - TypeScript/Vue: `camelCase` variables/functions, `PascalCase` components and `.vue` filenames, composables `use`-prefixed, types without `I`-prefix. - Abbreviations: only universally idiomatic ones (`id`, `url`, `api`, `db`, `llm`, `sse`). Never invented ones. - Single-letter names only in lambdas/comprehensions whose whole scope is ≤ ~3 lines. - Modules named for what they contain, singular nouns. `utils.py`, `helpers.py`, `misc.py` are banned names. ## Sizing, DRY, modularity - Soft cap ~300 lines per module, hard cap 500 — split by responsibility. Functions target ≤ ~40 lines. - One responsibility per module: if it needs "and" to describe, split it. - DRY by the rule of three: extract on the third occurrence, not the second. No premature abstraction, no helpers for one-shot operations, no designing for hypothetical future requirements. - Validate at boundaries only (user input, Spotify responses, LLM output); trust internal code — no defensive re-checking between our own modules. ## Comments & docstrings Sparse and load-bearing: - One-line docstring on every module and public function/class; add Args/Returns only where the signature genuinely doesn't tell the story. - Inline comments state only what the code cannot — constraints, API quirks, non-obvious whys. Never narrate the next line. - Banned: commented-out code, drive-by TODOs, decorative section banners. - Type hints everywhere; `mypy --strict` is the enforcement. ## Enforcement - Backend: `ruff` (lint + format) everywhere. `mypy --strict` on `domain/`, `ports/`, `pipeline/`; default strictness on `adapters/` and `api/` — SDK/streaming typing archaeology is not where the effort goes. Every `# type: ignore` carries an error code and a reason. - Frontend: `eslint` + `prettier` + `vue-tsc --noEmit`; no `any`. The stream event types are a discriminated union mirrored against the Pydantic response schemas. - Commit messages describe the change, conventional-commit style.