3.7 KiB
3.7 KiB
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/:Protocoldefinitions (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.yamlis one source of truth for three consumers: golden eval queries, demo fixture keys, and UI suggestion chips.
Naming
Human-readable, descriptive, full words.
- Python:
snake_casefunctions/variables,PascalCaseclasses,UPPER_SNAKEconstants. Functions verb-first (resolve_candidate), variables noun-first (grounded_tracks), booleans as predicates (is_grounded,should_stream). - TypeScript/Vue:
camelCasevariables/functions,PascalCasecomponents and.vuefilenames, composablesuse-prefixed, types withoutI-prefix. - Abbreviations: only universally idiomatic ones (
id,url,api,db,llm,sse), never invented ones. - Single-letter names only in lambdas and comprehensions whose whole scope is a few lines.
- Modules are named for what they contain, singular nouns.
utils.py,helpers.py, andmisc.pyare banned names.
Sizing, DRY, modularity
- Soft cap ~300 lines per module, hard cap 500. Split by responsibility. Functions target 40 lines or less.
- 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 unless it's a clear necessity.
Comments and 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 --strictis the enforcement.
Enforcement
- Backend:
ruff(lint + format) everywhere.mypy --strictondomain/,ports/,pipeline/; default strictness onadapters/andapi/, since SDK and streaming typing archaeology is not where the effort goes. Every# type: ignorecarries an error code and a reason. - Frontend:
eslint+prettier+vue-tsc --noEmit; noany. The stream event types are a discriminated union mirrored against the Pydantic response schemas. - Commit messages describe the change, conventional-commit style.