# Plan — build features Executes `architecture/build-features.md` against `quality/build-features.md`. Ordered by dependency; each task names how it is verified. Three commits: **(A)** the mount-registry refactor (no feature change), **(B)** the features themselves, **(C)** packaging + docs. Build commands run through devenv with the session-local target dir: ```sh devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … ' ``` ## A — Mount registry (behaviour-preserving refactor) - [x] **A1 — `Mount` + registry types.** In `provider.rs`: `struct Mount { root: &'static str, name: &'static str, client: Arc }`, `Mount::new(root, name, Arc) -> Mount` (the `Arc` coerces at the call site), and `Mount::owns(&self, path) -> bool` matching `path == root || path.starts_with("/")` — so `/fsx` is not `/fs`. *Verifies:* `mount_owns_its_root_and_children_only` (G10). - [x] **A2 — `ProviderOrchestrator` holds `mounts: Vec`.** Replace the nine `*_client` fields (keep `crabidy_store`). Add `from_mounts(Vec) -> Self` (sorts the mounts crabidy-first / orphans-last / rest alphabetical, and creates the bounded channel), used by `build` and the tests. *Verifies:* `root_lists_mounted_providers_in_order` (G12). - [x] **A3 — One dispatch helper.** `owner(&self, path) -> Option<&dyn ProviderClient>` plus `owner_or(&self, path, err)` that warns with the same message and returns the typed error. Delete the nine `*_owns` functions and the nine `*_provider` accessors. *Verifies:* `unowned_paths_are_typed_errors` (G11). - [x] **A4 — Rewrite the eight dispatch methods** (`is_track_path`, `get_urls_for_track`, `get_metadata_for_track`, `get_lib_node`, `create_lib_node`, `rename_lib_node`, `delete_lib_node`, `resolve_tracks_into`) as a single lookup each, preserving: the synthetic root short-circuit and `annotate_captured` in `get_lib_node`, `MalformedPath` for lookups, `NotSupported` for mutations, and the `warn!`s. *Verifies:* `dispatch_reaches_the_owning_mount`, `overridden_resolve_tracks_into_is_dispatched`, `no_mounts_serves_an_empty_root` (G10, G11, G13). - [x] **A5 — `get_lib_root` maps the sorted mounts** to `LibraryNodeChild::new` (no per-provider block, no re-sort). *Verifies:* `root_lists_mounted_providers_in_order` (G12). - [x] **A6 — `build()` registers mounts.** Each provider's init block stays as is; on success it pushes a `Mount` instead of assigning a field. `/crabidy` and `/orphans` push after the store exists. *Verifies:* full test suite + `cargo clippy -- -D warnings` (G2). - [x] **A7 — Commit A.** `cargo test -p crabidy-server`, clippy, fmt all clean; no `Cargo.toml` change in this commit (G1/G2 hold trivially). ## B — The features - [x] **B1 — Manifests** (done in api-design; re-verify): `crabidy-server` (`default = all-providers + opus + spectrum + web-ui`, gated deps optional), `audio-player` (`opus`), `cbd-tui` (`notifications`), `cbd` (pass-through of all of them). *Verifies:* G4, G5; `cargo tree -p crabidy-server` at defaults matches the pre-change tree (G1). - [x] **B2 — `settings`: `BUILT_IN_PROVIDERS`, `provider_enabled` bounded by it, `unavailable_providers()`, `ensure_default` writing only built-ins.** Replace the `todo!()`. *Verifies:* `an_absent_providers_key_enables_every_built_in_provider`, `unavailable_providers_reports_built_out_and_unknown_names`, `ensure_default_lists_only_built_in_providers`, `a_built_out_provider_cannot_be_enabled` (G4, G25, G26). - [x] **B3 — Per-provider `#[cfg]` in `build()` only.** Wrap each provider's init+registration block in `#[cfg(feature = "…")]`; the `enabled.*` toggle read stays inside the block. Provider `use`/imports move to the same gate. *Verifies:* the matrix (B12) compiles each provider alone; G9 by reading. - [x] **B4 — `fs`: gate the store half.** `#[cfg(feature = "fs")]` on the `crabidy_store`, `capture`, and `orphans` modules (`lib.rs`), on `ProviderOrchestrator::crabidy_store`/its field, on `Playback`'s store field and `restore_current` and the persister, and on the `/fs`, `/crabidy`, `/orphans` registrations. `Playback::new` keeps its arity — the store parameter becomes `#[cfg]`-free by threading `Option>` only under the feature (adjust the two call sites in `lib.rs` and the playback tests). *Verifies:* G14, G17; `--no-default-features` build (B12). - [x] **B5 — `fs`: RPC degradation.** Without `fs`, `CaptureLibraryNode` and `SaveQueue` return `Status::unimplemented` naming the feature; with `fs`, unchanged. No panic, no hang. *Verifies:* G15 by reading + a `--no-default-features` smoke run. - [x] **B6 — `fs`: the `scan` command.** Gate `AUDIO_EXTENSIONS`, `is_audio_file`, `scan`, `scan_dir`, `scan_file` and their tests behind `fs`; without it the dispatcher in `crabidy-server/src/main.rs` and `cbd/src/main.rs` prints "this binary was built without the `fs` feature" and exits non-zero. The clap surface does not change. *Verifies:* G16, G9. - [x] **B7 — `opus` in `audio-player`.** Gate `mod opus_source` and the `is_ogg_opus` sniff in `player_engine::build_source`; without the feature an Ogg-Opus header produces `Err` naming the missing feature (the existing "failed to decode" path), never a panic. *Verifies:* G18, G19 (read + the `opus`-off matrix entry); `opus_source`'s own tests stay under the feature. - [x] **B8 — `opus` in `scan`.** `AUDIO_EXTENSIONS` includes `"opus"` only under the feature. *Verifies:* `opus_is_scannable_only_with_the_decoder`, `scan_walks_past_opus_files_it_cannot_play` (G20). - [x] **B9 — `spectrum`.** Gate `mod spectrum`, `spawn_spectrum_task`, and its call; `realfft` optional. `Player::spectrum_tap()` unchanged. *Verifies:* G21; the `spectrum`-off matrix entry. - [x] **B10 — `notifications` in `cbd-tui`.** Gate the `notify_rust` import and the notify call in `app/now_playing.rs`; the config key stays accepted so an existing `cbd-tui.toml` still parses. *Verifies:* G23; `cbd-tui --no-default-features` build + `cargo test -p cbd-tui`. - [x] **B11 — `features` command.** `cli::build_features()` + `cli::features()` (replace the `todo!()`s), wired into `ServerCommand::Features` and `CbdCommand::Features`; plus one `info!` line at startup listing the build, and one `warn!` per `unavailable_providers()` entry. *Verifies:* `build_features_lists_providers_then_extras` (G24, G25). - [x] **B12 — The build matrix as a devenv script.** `check-features` runs `cargo clippy -- -D warnings` (and `cargo test` where meaningful) over: defaults · `--no-default-features` · each of the seven providers alone · defaults-minus-`opus` · minus-`spectrum` · minus-`web-ui` · `-p cbd-tui --no-default-features` · `-p audio-player --no-default-features`. Add `cargo-hack` to devenv for ad-hoc deeper sweeps. *Verifies:* G6, G7. - [x] **B13 — Commit B** once the whole matrix is green and no `todo!()`/`unimplemented!()` from the stub stage remains (G28). ## C — Packaging and docs - [x] **C1 — `flake.nix`.** Native build: replace the bare `--no-default-features` with an explicit `--no-default-features --features all-providers,opus,spectrum`; leave the aarch64 cross build on defaults (`web-ui` on). *Verifies:* G8; `nix build .#crabidy` if the sandbox allows, otherwise read + the equivalent cargo invocation. - [x] **C2 — `docs/src/build-features.md`** — the feature table, what each one costs to lose, the two worked examples (local-files appliance: `--no-default-features --features fs,opus`; streaming box: `--no-default-features --features tidal,web-ui,opus,spectrum`), the compile-time vs runtime distinction, and the `features` command. Linked from `SUMMARY.md`; `docs/src/config.md` cross-references it from the `providers` key. *Verifies:* G27 + `mdbook build`. - [x] **C3 — `README.md`** gains a short "tailored builds" pointer. *Verifies:* G27. - [x] **C4 — `plan/summary.md`** entry describing what shipped, the decisions taken autonomously, and what stayed out (auth, HLS, spectrum tap — with the reason). *Verifies:* dev-flow convention. - [x] **C5 — Commit C.** ## Deferred / not done (recorded, not silently dropped) - Gating `[auth]`/`argon2` — refused (fail-open security risk, D9). - Gating `hls.rs`, `windowed_http.rs`, `spectrum_tap.rs` — no dependency payoff (D7, D8). - A `minimal` convenience feature — users compose their own set (D1). - CI enforcement of the matrix — there is no CI yet; `check-features` is the manual gate.