crabidy/plan/search.md

131 lines
7.1 KiB
Markdown

# Plan — search via creatable nodes
Ordered tasks for the `implement` stage. Inputs: `architecture/search.md`,
stubs across all five crates, gates in `quality/search.md`. Tests:
`devenv shell -- cargo test -p crabidy-core -p tidaldy -p cbd-tui`
(11 failing at plan time = the target). Note: use a session-local
`CARGO_TARGET_DIR` if `target/` contains root-owned artifacts from the
repo owner's builds.
## 1. Path segment encoding (crabidy-core)
- [x] Add the `percent-encoding` crate (workspace dependency; check the
nixpkgs/devenv side is unaffected — pure Rust). Implement
`encode_segment` (encode `/`, `%`, whitespace, controls and everything
non-alphanumeric-unreserved via an `AsciiSet`) and `decode_segment`
(lossy UTF-8 decode). **Verify**: the three `crabidy-core` tests pass
(`encode_segment_round_trips_arbitrary_text`,
`encoded_segments_are_path_safe`, `decode_segment_is_lossy_not_panicky`);
gates "encode/decode are total", "only percent-encoding added".
## 2. Verify the search payload shape (tidaldy, live API)
- [x] Before typing the models: run the existing explorer helper
(`Client::search`) once against the live API (needs the local tidal
config; if unavailable, consult the response shapes used by other tdl
clients and mark the gate as verified-by-proxy). Confirm whether
`search/tracks|artists|albums` return the same `Page<T>` item shapes as
the library endpoints; put any deviation into `tidaldy::models`.
**Verify**: gate "models verified against the live API".
## 3. Typed search requests (tidaldy)
- [x] Implement `search_tracks/search_artists/search_albums`: single
`make_request` with `query`, `limit=SEARCH_RESULT_LIMIT`, `offset=0`,
decoding a `Page<T>`; no pagination loop. **Verify**: gate "first page
only"; unit-testable only against live API (leave network test
`#[ignore]` like the existing one).
## 4. Search subtree in the provider (tidaldy)
- [x] `is_track_path`: include `TidalPath::SearchTrack`; `track_id_from_path`
already falls out (its match is on the parsed variant — extend it).
**Verify**: `search_track_paths_are_track_paths`,
`track_id_is_extracted_from_search_track_paths` pass.
- [x] `get_lib_node` `Search` arm: children = one entry per stored term
(path `join_path(path, encode_segment(term))`, title = raw term,
`is_queable: false`), node `is_creatable: true`. Do not require
`user_id` for search paths (move the `get_user_id` gate into the arms
that need it — search must work even if the user id is missing).
**Verify**: gate "search node lists terms"; ignored network test
extended by hand.
- [x] `get_lib_node` `SearchTerm` arm: decode the term; implicitly register
unknown terms (stale-client recovery); fetch the three categories
concurrently (`tokio::join!`); node = tracks from `search_tracks`
(`to_proto(path)`), children = artists (`Artist: <name>` →
`/tidal/artists/<id>`, queueable) then albums (`Album: <title>` →
`/tidal/artists/<artist-id>/<album-id>`, queueable), `is_queable:
false`, `is_creatable: false`. **Verify**: gates "term node" +
"canonical children"; behavior exercised end-to-end in task 9.
- [x] `create_lib_node`: trim title → `InvalidInput` if empty; parent must
parse to `TidalPath::Search` else `NotSupported`; store raw term
idempotently (no duplicates, lock not held across await); return
`self.get_lib_node(term_path)`. **Verify**: gate "create semantics";
add non-network unit tests: create with bad parent / empty title on an
offline client returns the right errors (no API call happens before
validation).
## 5. Server plumbing (crabidy-server)
- [x] `ProviderOrchestrator::create_lib_node`: `/tidal`-prefixed parent →
`tidal_client.create_lib_node`, anything else `NotSupported` (warn like
the other routes). **Verify**: gate "orchestrator routing".
- [x] `rpc.rs create_library_node`: bounded(1) rendezvous with
`ProviderCommand::CreateLibraryNode`; map errors `NotSupported`
`failed_precondition`, `InvalidInput``invalid_argument`, rest →
`internal`. **Verify**: gate "error mapping"; `cargo check`.
## 6. TUI input overlay (cbd-tui)
- [x] `App::dispatch(LibraryCreateNode)`: open
`InputState { parent_path: library.path(), buffer: "" }` only when
`library.is_creatable()`. **Verify**:
`create_node_only_opens_input_on_creatable_nodes`.
- [x] `App::handle_input_key`: Esc cancels; Enter trims + sends
`MessageFromUi::CreateNode` (empty → just close); Backspace pops;
`KeyCode::Char(c)` appends regardless of SHIFT; all else ignored.
**Verify**: `input_appends_and_backspace_pops`,
`esc_cancels_without_sending`, `enter_submits_trimmed_title_and_closes`,
`enter_on_empty_input_closes_without_sending`.
- [x] Event loop (`main.rs run_ui`): when `app.input.is_some()`, route the
key to `app.handle_input_key(key)` and skip `bindings::lookup`
entirely. **Verify**: gate "input mode bypasses bindings" (read the
loop; also confirm `q` cannot quit while typing).
## 7. TUI library rendering (cbd-tui)
- [x] `Library::update`: apply empty nodes when `node.is_creatable` (keep the
skip for empty non-creatable nodes). **Verify**:
`empty_creatable_nodes_are_enterable`.
- [x] Mark creatable children in the list (suffix marker, `COLOR_SECONDARY`)
— requires keeping `is_creatable` on `UiItem`; hint in the pane title
when the open node is creatable (e.g. `search — % to add`).
**Verify**: gate "creatable marked in UI" (read render code).
- [x] Render the input overlay: bottom line of the library pane showing
`new node: <buffer>▏` while `input.is_some()`. **Verify**: gate "input
overlay renders"; add a `TestBackend` test asserting the buffer text
appears while open (mirror the help-modal test helpers).
## 8. TUI ↔ server wiring (cbd-tui)
- [x] `RpcClient::create_library_node`: send request, evict
`library_node_cache` entry for `parent_path`, insert the returned node,
return it. **Verify**: gate "cache eviction" (read; the cache is
private — no test seam without refactoring, keep it a gate).
- [x] `main.rs poll`: `CreateNode` arm calls the client method; on success
`ReplaceLibraryNode(node)`; on failure log and leave the UI unchanged
(no panic — replace the stub's `todo!`). **Verify**: gate "create
failures surface as errors, never a panic".
## 9. End-to-end + gates sweep
- [x] Run the real stack (`crabidy-server` + `cbd-tui`, needs tidal login):
enter `/tidal/search`, `%`, type a term, Enter; results appear; queue a
track result; dive into an artist result. If no login is available,
exercise create/list/error paths against the offline client instead
and note it in the summary. **Verify**: architecture flow diagram
matches reality.
- [x] Sweep: no `todo!()` left (workspace grep), fmt + clippy + tests green,
every `quality/search.md` box checked, docs updated where behavior
shifted. Append the outcome + deviations to `plan/summary.md`.