docs: design contract and driver conventions
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>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# 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 `sqlx` compile-time-checked queries.
|
||||
|
||||
## Working an issue
|
||||
|
||||
1. Read `DESIGN.md`, then the issue, then the sections the issue cites.
|
||||
2. Branch from `main`. One issue per branch.
|
||||
3. Implement the issue as scoped. Do not widen it. If you find adjacent work,
|
||||
open a new issue and keep going.
|
||||
4. `just ci` must pass locally before pushing. That runs the same gate as CI.
|
||||
5. 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:
|
||||
|
||||
1. List open issues. Drop any whose `Depends on:` references an open issue.
|
||||
2. Of what remains, take the lowest phase number first, then whatever
|
||||
parallelises within it.
|
||||
3. Spawn one session per issue, selecting the model from `difficulty/`.
|
||||
4. 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-movies` work until `phase/2-logic` is 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** — `prowlarr` container 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.
|
||||
Reference in New Issue
Block a user