| name | coding-guidelines |
| description | Use this skill for any implementation task — adding features, fixing bugs, modifying resources, changing pipeline stages, or updating channels. Always consult this before writing code. |
Workflow
- Write tests first. The test is the executable specification. If you can't write the test, the requirements aren't clear enough — stop and clarify before writing any implementation code. Load the
automated-testing skill for patterns and policies.
- Implement the minimum change to make the tests pass.
- Run
mix precommit before finishing. Fix all warnings and failures — zero warnings policy.
Modular Cohesion
Before adding a function, handler, or piece of state to an existing
module, read the moduledoc. If it already names more than one
responsibility — look for "and" between concerns, numbered lists like
"Two responsibilities:", or # --- section dividers separating
distinct domains — that's a smell, and the new code is the cheapest
moment to fix it. Split the module into collaborators as part of the
change, not as follow-up work.
The split rule:
- Each module's
@moduledoc names one thing it owns.
- Relationships are one-way: the consumer module names the
data-source module in its moduledoc; the data-source module says
nothing about the consumer.
- Tests live next to the module they describe. Integration tests
assert the contract between modules, never the internals (per
ADR-026).
Canonical examples in this repo:
| Consumer | Data source | Pattern |
|---|
MediaCentaur.Watcher.AbsencePolicy | MediaCentaur.Watcher.FilePresence | TTL/lifecycle policy uses presence-tracking primitives. |
MediaCentaur.Library.Availability | MediaCentaur.WatcherStatus | Library reads watcher state through a boundary-neutral helper. |
When in doubt: write the moduledoc you'd want to see for the new code
first. If you can't fit it in one short sentence, the work belongs
in (or as) a separate module.
Test Patterns by Domain
Ecto Schemas (Movie, TVSeries, MovieSeries, VideoObject, WatchedFile, WatchProgress, Image)
- Use
DataCase (not async — SQLite limitation).
- Use
create_* factory helpers to persist via the relevant context module
(MediaCentaur.Library, MediaCentaur.Review, MediaCentaur.ReleaseTracking,
MediaCentaur.Settings).
- Test through the context's public API against the real database — never stub
the data layer, never call
Repo directly from tests.
- For bulk operations, wrap in
Ecto.Multi and assert on the transaction result.
Pipeline Stages (Parse, Search, FetchMetadata, DownloadImages, Ingest)
- Call stage
run/1 or Pipeline.process_payload/1 directly — no Broadway topology in tests.
- Stub TMDB with
TmdbStubs helpers (stub_search_movie/1, stub_routes/1, etc.) — never mock.
- Images use
NoopImageDownloader via config — no HTTP or file I/O.
- Test orchestration and state transitions, not leaf functions (Parser, Mapper have their own suites).
- Never delete or weaken pipeline tests.
Pure Functions (Parser, Serializer, Mapper, Confidence, Resume)
- Use
async: true with ExUnit.Case.
- Use
build_* factory helpers — plain structs, no database.
LiveView Logic Extraction (Mandatory)
Extract all non-trivial LiveView/component logic into public pure functions and unit test them ([ADR-030]). LiveViews are thin wiring. Any if/case/cond on domain data → extracted function with a test.
What NOT to Test
- GenServer internals (Watcher, Config, MpvSession).
- Rendered HTML — no
render_component, no =~ on markup. Integration tests (mount, patch, events) are fine.
- External API calls in normal runs — tag
@tag :external and exclude.
Factory
All tests use MediaCentaur.TestFactory. Never inline Ecto.Changeset.cast / Repo.insert! boilerplate.
build_* — pure structs for async tests (fast, no I/O).
create_* — persisted via context-module functions for DataCase tests.