CONVENTIONS.md
Rama main · réplica de lectura del repositorio original.
# 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_<action>_<expected outcome>`, 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
<!-- Language-specific conventions are appended per-repo by the sync workflow -->
## 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