crabidy/plan/build-features.md

8.5 KiB

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:

devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … '

A — Mount registry (behaviour-preserving refactor)

  • A1 — Mount + registry types. In provider.rs: struct Mount { root: &'static str, name: &'static str, client: Arc<dyn ProviderClient> }, Mount::new(root, name, Arc<impl ProviderClient>) -> Mount (the Arc coerces at the call site), and Mount::owns(&self, path) -> bool matching path == root || path.starts_with("<root>/") — so /fsx is not /fs. Verifies: mount_owns_its_root_and_children_only (G10).
  • A2 — ProviderOrchestrator holds mounts: Vec<Mount>. Replace the nine *_client fields (keep crabidy_store). Add from_mounts(Vec<Mount>) -> 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).
  • 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).
  • 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).
  • 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).
  • 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).
  • 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

  • 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).
  • 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).
  • 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.
  • 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<Arc<CrabidyStore>> only under the feature (adjust the two call sites in lib.rs and the playback tests). Verifies: G14, G17; --no-default-features build (B12).
  • 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.
  • 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.
  • 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.
  • 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).
  • B9 — spectrum. Gate mod spectrum, spawn_spectrum_task, and its call; realfft optional. Player::spectrum_tap() unchanged. Verifies: G21; the spectrum-off matrix entry.
  • 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.
  • 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).
  • 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.
  • B13 — Commit B once the whole matrix is green and no todo!()/unimplemented!() from the stub stage remains (G28).

C — Packaging and docs

  • 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.
  • 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.
  • C3 — README.md gains a short "tailored builds" pointer. Verifies: G27.
  • 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.
  • 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.