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. Code reads as prose.
- 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/comprehensions whose whole scope is ≤ ~3 lines.
- Modules named for what they contain, singular nouns.
utils.py,helpers.py,misc.pyare 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 --strictis the enforcement.
Enforcement
- Backend:
ruff(lint + format) everywhere.mypy --strictondomain/,ports/,pipeline/; default strictness onadapters/andapi/— SDK/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.