# Code Conventions Shared conventions for all tskovlund repositories. ## How to read these - **Rules encode intent; follow the intent.** A rule satisfied by a workaround is a rule broken: `any` to "use TypeScript", a docstring that restates the signature to "document the public API", a test that asserts a constant to "cover the module". When the letter and the intent conflict, do what the intent asks and say so in the PR - **Nothing is maintained for its own sake.** Every line of code, test, documentation and configuration must earn its place. What adds no value is deleted, not kept current - **Generate over hand-maintain.** Anything derivable from a source of truth is generated from it, with a check that fails when the copy drifts: API references from docstrings, release notes from commits, CI matrices from a versions file - **Proper fixes over workarounds.** Solve at the root cause. When a workaround is unavoidable (the cause is outside our control), say so in the code and track it so it can go once a real fix exists ## Code - **Explicit types.** The reader never guesses a type: full annotations in Python, no `var` in C#, no `auto` where the type is not obvious. Escape hatches (`Any`, `any`, casts) only at true boundaries such as JSON, and narrowed immediately - **Full names.** `measure_index` not `m`, `exception` not `exc`, `message` not `msg`. Intent is read, not inferred - **No magic constants.** Ports, protocol versions, error codes, timeouts are named - **One source of truth.** Shared logic lives in one place; prefer a base class or a shared function over copy-paste - **Single responsibility.** A file, class or function does one thing; an interface exists where it decouples something that varies - **Comments and docstrings explain semantics**: what a value means, which constraints hold, why the code does what it does. They never retell what the signature already says - **Idempotency.** Scripts, migrations and deployments are safe to run twice - **Tooling enforces the mechanical rules**: import order, formatting, linting, type checking, no debug printing (`no-console`, Ruff `T20`). Warnings fail CI; a warning that stays becomes invisible ## Configuration - **Externalize what varies by environment or that users may change**; hardcode what is truly constant - **Pick the mechanism for the situation**: environment variables for secrets and deployment, config files for complex settings, CI variables for build-time values ## Errors and resilience - **Surface problems.** Errors are visible, never swallowed - **Validate at boundaries** (API inputs, external data); trust internal code - **Structured logging** through the platform's logging framework, with consistent levels - **Retry policies come from a library** (tenacity, Polly). A single bounded reconnect that a protocol calls for is fine; a homebrew retry loop with backoff is not ## APIs - **Predictable and consistent**: one naming scheme, one error shape, one return convention across every endpoint or tool - **Version deliberately.** Whether versioning adds value depends on the technology; make it a decision, not a default ## Dependencies - **Battle-tested libraries for problems with known edge cases** (auth, retries, serialization); **no dependency for a few lines of our own code** - **Vet before adding**: maintenance activity, security record, transitive dependency count. Ask the maintainer when unsure - **Lockfiles committed** (`uv.lock`, `package-lock.json`, `flake.lock`) - **Direct dependencies pinned to compatible ranges with a major upper bound** (`"websockets>=14.0,<15"`, `"^14.0.0"`), so Renovate produces reviewable PRs instead of silent breakage. GitHub Actions pinned to a commit SHA with a version comment - **Renovate and CodeQL in CI** where available. No secrets in code ## Testing - **Cover the critical paths.** A test exists to catch a specific regression: every behavior a user would notice failing, every error path production can hit, every edge case that has bitten. A coverage percentage is not a goal - **Every test earns its place.** No trivial tests, no tests of library behavior, no assertions on constants. Shared logic is tested once, not per subclass - **Unit tests are fast and offline**; they work on a plane - **Integration tests exercise the real thing** (a real MuseScore, a real database via Testcontainers) where a mock cannot catch the failure that matters. They are separate and opt-in so the fast loop stays fast - **Naming and shape**: `test__`, cased per language; Arrange / Act / Assert - **One gate, identical locally and in CI**: lint, format, typecheck and tests behind one command (`devbox run check`, `mix precommit`, whatever the repo's tooling provides). Dev shells (Devbox, Nix) make the tooling identical - **Archetype minimums**: static websites get accessibility checks (axe) and end-to-end navigation; Lean projects get `lake test` and a no-`sorry` check; Nix configurations get `nix flake check --all-systems` ## Git and releases - **Conventional commits**, enforced by the shared `commit-msg` hook: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`, `style`, `perf`, `build`, `revert`, with an optional scope - **Branch, PR, required checks, squash merge** is the default; one small PR per change. A personal configuration repo may allow direct pushes to `main`; that is a per-repo ruleset decision, made deliberately - **Releases are tags** (`vX.Y.Z`, semver, `0.x` until the API is stable). Release notes live on the GitHub release: drafted from the conventional commits since the previous tag, curated by hand, published once. No hand-maintained changelog file ## Documentation - **Docs state what the code does not.** Rationale, constraints, how to operate the thing. Anything derivable is generated (see above). No counts or version lists that drift; describe or point at the source instead - **Diataxis as a thinking tool**: tutorial, how-to, reference, explanation. Not a filing system - **README sells, `docs/` teaches.** The README says what the project is, why it matters, and gets a reader running in a few commands. Details live in `docs/` and are linked; a small repo may need no `docs/` - **CONTRIBUTING.md when external contributors are expected**: dev setup, the gate, the PR process; it links to CONVENTIONS.md rather than restating it - **Author section at the bottom of every README**: `Name — [site](url) · [email](mailto:email)`, followed by a License section in licensed repos ## Repositories - **Language conventions apply** (`src/` layout in Python, the framework's structure elsewhere) - **AGENTS.md in every repo** holds the instructions for AI agents (Claude Code reads it directly; no CLAUDE.md symlink). Repos are de-personalized factsheets; kammer's AGENTS.md is the deliberate exception, an owner-specific operations manual - **Shared CI via `tskovlund/.github`**: reusable workflows instead of duplicated configuration - **Conventions are synced into each repo** as its CONVENTIONS.md, general plus the repo's language modules, so every contributor and agent sees them without leaving the repo ## Elixir - **ExUnit idiom over the generic test template** — `describe` blocks with sentence-style `test "..."` names replace `test_action_expected_outcome` naming, and Arrange/Act/Assert comments are omitted: well-factored ExUnit tests read as specifications. This is the "cased per language convention" rule applied to Elixir - **The full gate is `mix format`, Credo strict, Dialyzer, Sobelow, warnings-as-errors, and the test suite** — run via `mix precommit` (or `make check`), identically local and in CI - **`@moduledoc`, `@doc`, and `@spec` on every public module and function** - **One authorization boundary** — permission and visibility decisions live in a single module; no inline permission checks in templates, LiveViews, or controllers - **Contexts own the writes** — the web layer calls context functions; Ecto queries do not leak into LiveViews or controllers - **Property-based tests (StreamData)** for invariants that deserve the rigor (permissions, visibility rules) - **Security scanning is Sobelow + `mix hex.audit` + `mix deps.audit`** in CI — CodeQL has no Elixir support - **No `Process.sleep/1` or `Process.alive?/1` in tests** — they are the primary flakiness source. Wait for a process with `Process.monitor/1` + `assert_receive {:DOWN, ...}`; synchronize with `_ = :sys.get_state(pid)`; start test processes with `start_supervised!/1` so cleanup is guaranteed - **Ecto discipline** — preload every association a serializer or template touches; read changeset fields with `Ecto.Changeset.get_field/2`, never map access; programmatically-set fields (e.g. `user_id`) are set explicitly, never listed in `cast` (mass-assignment risk); note `validate_number/2` has no `:allow_nil` option — validations already skip nil changes