Design for a single-service replacement of Radarr/Sonarr: domain model, policy engine, sourcing via Prowlarr Torznab, import pipeline, reconcile loop, crate layout, CI gate and build order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.1 KiB
arr
Single-service replacement for Radarr and Sonarr, later Bazarr. Rust workspace, API-first, SQLite, no authentication layer.
Read DESIGN.md before touching anything. It is the contract. Issues
reference its sections rather than restating them. If an issue and the design
document disagree, the design document wins and the issue is wrong — say so
rather than implementing the discrepancy.
Repo layout
crates/
├── arr-core/ domain types, policy engine, scoring (no IO, no heavy deps)
├── arr-parse/ release name parsing (no IO)
├── arr-meta/ TMDB client
├── arr-indexer/ Torznab via Prowlarr
├── arr-dl/ Transmission RPC
├── arr-probe/ ffprobe wrapper
├── arr-db/ sqlx + migrations
├── arr-api/ axum + OpenAPI
├── arr-compat/ Radarr/Sonarr v3 shim for Jellyseerr
├── arr-daemon/ reconcile loop, wires everything
└── arr-e2e/ cross-process integration tests
web/ Vite + TypeScript SPA, embedded via include_dir
The invariant that matters: arr-core and arr-parse must never depend on
axum, sqlx or reqwest. They hold the logic that gets tested constantly and they
must stay fast to compile. Everything expensive is downstream of them. A pull
request that adds a heavy dependency to either is wrong.
Conventions
- Dependency versions are pinned once in the root
[workspace.dependencies]. Member crates reference them by name, never by version. - Lints are declared once in the root
[workspace.lints]. Member crates carry only[lints] workspace = true. - Commits follow Conventional Commits, subject ≤ 50 characters, body only when the "why" is not obvious from the diff.
- No
unwrap()in non-test code. The lint is on. - SQL goes through
sqlxcompile-time-checked queries.
Working an issue
- Read
DESIGN.md, then the issue, then the sections the issue cites. - Branch from
main. One issue per branch. - Implement the issue as scoped. Do not widen it. If you find adjacent work, open a new issue and keep going.
just cimust pass locally before pushing. That runs the same gate as CI.- Open a pull request referencing the issue number.
If an issue turns out to be blocked by something not yet built, say so on the issue and stop. Do not implement the dependency inline — it has its own issue, or it needs one.
Label taxonomy
Every issue carries exactly one phase/, one area/, one difficulty/, and
one type/. Dependencies are expressed in the issue body as Depends on: #N.
phase/ — build order, from DESIGN.md §13
| Label | Meaning |
|---|---|
phase/1-skeleton |
workspace, CI, config, database, empty API and SPA |
phase/2-logic |
parsing and the policy engine, pure, no network |
phase/3-sourcing |
TMDB and Prowlarr, read-only, grabs nothing |
phase/4-movies |
movies end to end, first real cutover test |
phase/5-ui |
search, buckets, library views, queues |
phase/6-tv |
seasons, episodes, tracking, derived status |
phase/7-people |
owner tags and notifications |
phase/8-compat |
Jellyseerr shim |
Phases are ordered but not strictly serial — issues within a phase often
parallelise, and some later-phase issues unblock early. Depends on: is
authoritative, the phase label is a hint.
area/ — which crate
area/core, area/parse, area/meta, area/indexer, area/dl,
area/probe, area/db, area/api, area/compat, area/daemon, area/web,
area/ci, area/infra
difficulty/ — drives model selection
| Label | Shape of the work |
|---|---|
difficulty/trivial |
one file, mechanical, no design decisions |
difficulty/easy |
bounded, obvious approach, few files |
difficulty/moderate |
multiple files, some judgement, a real interface to design |
difficulty/hard |
subtle correctness, cross-cutting, or the design document itself is thin here |
type/
type/feature, type/chore, type/test, type/bug
Driving this autonomously
A driver session should be able to work from DESIGN.md plus the issue list
alone. The loop:
- List open issues. Drop any whose
Depends on:references an open issue. - Of what remains, take the lowest phase number first, then whatever parallelises within it.
- Spawn one session per issue, selecting the model from
difficulty/. - Wait for CI green on the pull request before marking the issue done and recomputing the ready set.
Model selection
| Difficulty | Model |
|---|---|
difficulty/hard |
Opus 5 |
difficulty/moderate |
Sonnet 5 |
difficulty/easy |
Sonnet 5 |
difficulty/trivial |
Haiku 4.5 |
Escalate one tier if a session fails CI twice on the same issue. Never de-escalate mid-issue.
The operator mentioned a model named "terra" for the lower tiers. No such model was identified — confirm what it refers to and correct this table before relying on it.
What a driver must not do
- Do not run more than one session per crate at a time. Cargo workspace builds contend, and two sessions editing the same crate produce conflicts that cost more than the parallelism saved.
- Do not start
phase/4-movieswork untilphase/2-logicis complete. The policy engine is the thing everything else calls. - Do not merge a pull request that skips tests to get CI green. Failing tests are the signal the design document is wrong somewhere; surface it.
Environment
- Prowlarr —
prowlarrcontainer on the Dokploy host, port 9696. Owns tracker auth, FlareSolverr and the Cardigann definitions. Not replaced. - Transmission — native in LXC 130 at
10.6.10.45:9091, RPC unauthenticated. Download dir/mnt/media/transmission/complete. - Jellyfin — native in LXC at
10.6.10.18:8096. Library roots under/mnt/media-v2. - Media — ZFS, single dataset, bind-mounted as
/mnt/media. Downloads and library share it, so hardlinks work. - ntfy — running on the Dokploy host, one topic per person.
Development happens on the same machine that runs the stack, so all of the above are directly reachable. Never point tests at a live tracker.