discovery-by-llm/AGENTS.md
2026-08-09 20:56:13 +02:00

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/: 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.

  • 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 and comprehensions whose whole scope is a few lines.
  • Modules are named for what they contain, singular nouns. utils.py, helpers.py, and misc.py are 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 --strict is the enforcement.

Enforcement

  • Backend: ruff (lint + format) everywhere. mypy --strict on domain/, ports/, pipeline/; default strictness on adapters/ and api/, since SDK and 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.