# Task plan — the `orphans` provider Implements `architecture/orphans.md`, satisfying `quality/orphans.md`. Ordered by dependency. Each task names its verification. All `cargo`/tooling runs go through `devenv shell -- …` (see `CLAUDE.md`). > **Done.** All tasks implemented and verified: `cargo test --workspace`, > `cargo clippy --workspace --all-targets -- -D warnings`, `cargo fmt --check`, > and `mdbook build docs` all pass. See "Deviations" at the end. ## Store layer (`crabidy-server/src/crabidy_store.rs`) - [x] **T1 — `OrphanEntry` + `StoreIndex::remove`.** Move/define `OrphanEntry` (name + display metadata) and add `StoreIndex::remove(&mut self, name, sidecar)`, the inverse of `insert` (drop `by_hash[hash]` and each `by_provider_id[(provider,id)]` that still maps to `name`). *Verify:* unit test that `insert` then `remove` leaves the index empty; clippy/fmt. - [x] **T2 — `list_orphans(&self, ref_roots) -> Result, StoreError>`.** Scan `store_root` for `*.cbd-store.toml` (name + parsed sidecar); require the audio file present unless reporting malformed residue (G5). Build the referenced set by walking each `ref_root` recursively for `*.cbd-track.toml`, parsing each and collecting `Playable::Store(name)` (skip unreadable/bad files with a warning). Return all − referenced, metadata from `providers[0]`. *Verify:* G1–G6 tests. - [x] **T3 — `orphan_track` / `orphan_url`.** `orphan_track(name)` reads the sidecar and builds a `Track` (metadata from `providers[0]`, `is_captured = true`, path left to the provider to set). `orphan_url(name)` returns `store_root/` as a string, erroring `NoDir`/`MalformedPath`-style if the entry is gone. *Verify:* G8/G9 tests. - [x] **T4 — `rename_orphan(&self, old, new)`.** Under the index mutex: `validate_folder_name(new, &[])`; refuse if `name_taken(new)` (G13); rename audio and sidecar (`tokio::fs::rename` both, cross-device fallback to copy+remove like `ingest_file`); read the sidecar, `index.remove(old, &sidecar)`, `index.insert(new, &sidecar)`. *Verify:* G11–G13 tests. - [x] **T5 — `delete_orphan(&self, name)`.** Under the index mutex: read the sidecar (for index removal; tolerate a missing sidecar → still remove any stray audio), remove audio + sidecar (idempotent on `NotFound`), `index.remove(name, &sidecar)`. *Verify:* G15–G16 tests. ## Provider (`crabidy-server/src/orphans.rs`) - [x] **T6 — `entry_name` + leaf/root nodes.** Fill `entry_name` (decode the one segment; reject bare root, deeper, or separator-bearing paths → `None`). Implement `get_lib_root` and `get_lib_node`: root lists children from `store.list_orphans(&self.ref_roots)` (flags per G7, path `/orphans/`, title `name`); leaf returns a childless queueable node with the single `orphan_track` (path set to the leaf path). *Verify:* G7/G8 tests; `MalformedPath` for unknown/`..` segments (G17). - [x] **T7 — play/queue + mutations.** Implement `get_urls_for_track` (`orphan_url`), `get_metadata_for_track` (`orphan_track`), `rename_lib_node` (validate → `rename_orphan` → return new node), `delete_lib_node` (`delete_orphan` → return refreshed root). *Verify:* G9, G11–G17 tests via the provider surface. ## `fsdy` accessor (`fsdy/src/lib.rs`) - [x] **T8 — `Client::disk_root(&self) -> &Path`.** Public accessor returning the instance's disk root, so the orchestrator can hand the `/fs` root to the orphans provider as a reference root. Doc comment (G20). *Verify:* trivial; compiles + fmt. ## Orchestrator wiring (`crabidy-server/src/provider.rs`, `lib.rs`) - [x] **T9 — Declare the module.** Add `mod orphans;` (and any `pub use`) to `crabidy-server/src/lib.rs`; this un-inerts the stub. *Verify:* workspace compiles. - [x] **T10 — Mount + route `/orphans`.** Add `orphans_client: Option>` to `ProviderOrchestrator`; construct it in `init` when `crabidy_store` is `Some`, with `ref_roots` = `[store.tree_dir()]` plus `fs_client.disk_root()` when present. Add `orphans_owns(path)`; route `is_track_path`, `get_lib_node`, `get_urls_for_track`, `get_metadata_for_track`, `create/rename/delete_lib_node`, and `resolve_tracks_into` to it. Add the `orphans` child to `get_lib_root` when mounted. Note: keep `annotate_captured` running on orphan nodes (already correct — store-backed tracks are `is_captured`). *Verify:* G21 by reading; workspace compiles; existing provider tests still pass. ## Docs - [x] **T11 — mdbook + README.** Add an `/orphans` section to `docs/src/store.md` (or a short `docs/src/providers/orphans.md` linked from `SUMMARY.md`) and a line to `README.md`'s provider tree describing `/orphans` as the store's reclamation view (rename/delete/queue unreferenced audio). *Verify:* `devenv shell -- mdbook build docs` + markdownlint clean. ## Final gate - [x] **T12 — Full verification.** `devenv shell -- cargo test --workspace`, `cargo clippy --workspace -- -D warnings`, `cargo fmt --check`, markdownlint. Re-read `quality/orphans.md` G1–G21 and check each. Update `plan/summary.md`. Commit with the co-author trailer. ## Deviations from the plan / architecture - **Test placement (T2/T4/T5).** Rather than one test per store method, the provider-level tests in `orphans.rs` exercise `list_orphans`/`orphan_track`/ `orphan_url`/rename/delete end-to-end through the `ProviderClient` surface (list vs. reference, leaf-track resolution, rename-both-files + delete, traversal rejection). One store-level test (`rename_orphan_moves_both_files_and_repoints_the_index`) additionally asserts G12 (index repoint) by inspecting the private index. All of G1–G21 are covered; the split just differs from the per-method layout the plan implied. - **`list_orphans` enumerates a union (G5).** It walks both `*.cbd-store.toml` sidecars and bare audio files, so a sidecar without audio *and* audio without a sidecar both surface as reclaimable — as the architecture's "malformed residue" note requires. - **Docs home (T11).** The user-facing section landed in `docs/src/store.md` ("Reclaiming orphans — /orphans") with a bullet in `providers.md` and a line in the README provider tree, rather than a standalone `docs/src/providers/orphans.md` page — `/orphans` is a view over the store, so it reads best beside the store doc, and no new `SUMMARY.md` entry was needed. - **No proto/TUI/web changes (G10 held).** The feature reuses the existing `is_editable`/`is_deletable`/`is_queable` child handling end-to-end; the only non-server change is the additive `fsdy::Client::disk_root` accessor.