125 lines
6.9 KiB
Markdown
125 lines
6.9 KiB
Markdown
# Quality gates: queue de-duplication and sorting
|
||
|
||
Criteria an implementation of `architecture/queue-order.md` must satisfy.
|
||
Automatic tests cover the behaviour (see "Tests" at the end); the gates here
|
||
are the things a reader has to check by reading the code. Each is pass/fail.
|
||
|
||
## Correctness of the operations
|
||
|
||
- [ ] **G1 — Dedup never removes the playing entry.** In every group of
|
||
duplicates the survivor is the entry at the current position when it is in
|
||
the group, otherwise the earliest one (D4). Check the survivor selection
|
||
directly, not only through a test: a "keep the first" shortcut is a silent
|
||
playback stop the moment the playing copy is a later one.
|
||
- [ ] **G2 — The default duplicate identity is provider-scoped.** The key is
|
||
`(first path segment, provider_item_id)` when the id is non-empty and the
|
||
whole path otherwise (D3). Artist/title matching exists **only** under the
|
||
opt-in `by_title` identity (D3a), never on the default path, and the flag is
|
||
never defaulted to true anywhere between key and wire.
|
||
- [ ] **G2a — `by_title` keeps the longest take** and still yields to the
|
||
playing entry (D3a), and every client surface that reaches it is distinct
|
||
from the safe one (its own key, its own flag) — a user cannot get it by
|
||
mistyping.
|
||
- [ ] **G3 — Sort is stable and total.** `sort_permutation` returns a
|
||
permutation of `0..len` for every strategy and direction, equal keys keep
|
||
their queue order, and blanks/unknown durations sort last in *both*
|
||
directions (D7, D8). No comparator may panic (no `unwrap` on a partial
|
||
comparison, no float keys).
|
||
- [ ] **G4 — Sort keys are computed once per track.** Decorate–sort–undecorate,
|
||
not a comparator that lowercases inside the comparison (A5). A ten-thousand
|
||
entry queue must not allocate per comparison.
|
||
- [ ] **G5 — Reverse ignores the direction flag** (D6), and
|
||
`QueueSort::Unspecified` reaching `QueueManager::sort` is a no-op rather
|
||
than an arbitrary default.
|
||
- [ ] **G6 — The play-order invariant holds after both operations.**
|
||
`play_order` indexes every track exactly once and `current_offset` points
|
||
into it (the invariant `shuffle_insert_keeps_order_unique` guards for
|
||
inserts). Shuffle off ⇒ rebuilt from the new order; shuffle on ⇒ remapped
|
||
through the permutation, so the remaining play sequence is unchanged (D10).
|
||
- [ ] **G7 — Dedup goes through `remove_tracks`.** There is exactly one place
|
||
that removes queue entries (D5). A second removal implementation that
|
||
maintains `play_order` itself is a fail even if its tests pass.
|
||
|
||
## Server discipline
|
||
|
||
- [ ] **G8 — Both operations run on the playback loop and nowhere else.** No
|
||
queue mutation outside a `PlaybackCommand` arm; the queue lock is never held
|
||
across an `await` (the pattern every existing arm follows).
|
||
- [ ] **G9 — One broadcast site.** Both go out through `broadcast_queue`, so
|
||
the persister and the update stream cannot drift (D12). A dedup that removed
|
||
nothing broadcasts nothing.
|
||
- [ ] **G10 — No panics on client input.** `SortQueue` with an unset or
|
||
unknown enum value answers `InvalidArgument`; `DedupQueue` with a dropped
|
||
result receiver logs and continues; an empty queue is a no-op for both. No
|
||
`unwrap`/`expect`/indexing that a client can reach.
|
||
- [ ] **G11 — Both RPCs are in the rights matrix as `QueueOwner`** (D1), and
|
||
the pinned method-list test in `auth.rs` includes them. A new RPC must not
|
||
be able to reach the wire unmapped.
|
||
- [ ] **G12 — The dedup count crosses the boundary as a number.** No
|
||
server-formatted human string on the wire or in a `MessageToUi` (D2, D15);
|
||
wording belongs to each client.
|
||
- [ ] **G13 — Nothing is logged that could carry a credential.** Sort logs the
|
||
strategy, dedup logs a count. No track paths, no URLs — a queue entry's path
|
||
can be a `/rss` item whose URL is a per-subscriber token.
|
||
|
||
## Clients
|
||
|
||
- [ ] **G14 — No client computes an order.** No comparators, no
|
||
duplicate-finding, and no `Remove`-derived dedup in `cbd-tui`, `cbd-web` or
|
||
`cbd-cli` (A1, Option A rejected). Clients send the verb and render what
|
||
comes back.
|
||
- [ ] **G15 — The sort menu is strictly modal.** While it is open the bindings
|
||
table is unreachable and an unclaimed key leaves it open rather than falling
|
||
through — a stray `c` must not clear the queue while the user thinks they are
|
||
picking a sort (D14).
|
||
- [ ] **G16 — Both operations are reachable without a keyboard in the web
|
||
client** (D16): toolbar buttons dispatching the same actions as `u` and `S`,
|
||
and the strategy list exists once (`SORT_CHOICES`), driving both the overlay
|
||
and the button.
|
||
- [ ] **G17 — The TUI and web keymaps agree**: `u` = dedup, `S` = sort menu,
|
||
and the same five menu letters with the same uppercase-is-descending rule.
|
||
Where they differ, the difference is deliberate and documented.
|
||
- [ ] **G18 — Feedback exists for both outcomes of a dedup.** `0` renders as
|
||
plainly as `7` (D2/D15) — a dedup that found nothing must not look like a
|
||
dropped keypress.
|
||
- [ ] **G19 — The CLI strategy is a `ValueEnum`** (D17), so a typo is a parse
|
||
error, the shell completes it, and the printed confirmation names the
|
||
strategy in the same spelling the user typed.
|
||
- [ ] **G20 — Every new binding is documented where the others are**: the TUI
|
||
help modal (derived from `BINDINGS`, so automatic), the web help overlay
|
||
table, `docs/src/clients/tui.md`, `docs/src/clients/cli.md`, and
|
||
`docs/src/queue.md`'s queue-operation list.
|
||
|
||
## Documentation and honesty
|
||
|
||
- [ ] **G21 — The proto documents the semantics at the wire.** What a
|
||
duplicate is, which copy survives, that unknowns sort last, that `Reverse`
|
||
ignores `descending`, and that `UNSPECIFIED` is refused.
|
||
- [ ] **G22 — The known gaps are written down, not left to be discovered**: a
|
||
captured copy and its streaming source are not duplicates (D3), a sort with
|
||
shuffle on does not change what plays next (D10), and tracks still resolving
|
||
land unsorted after the fact (D11).
|
||
- [ ] **G23 — Public items have doc comments** stating intent and edge
|
||
behaviour, in the register the surrounding code uses (why, not what).
|
||
|
||
## Tests
|
||
|
||
Behaviour is pinned by these, all of which must pass:
|
||
|
||
| Area | Test home | Pins |
|
||
| --- | --- | --- |
|
||
| Identity, survivors, permutations | `queue_order.rs` | D3, D4, D6–D9 |
|
||
| Play-order invariants | `lib.rs` (`QueueManager`) | D5, D10, empty queues |
|
||
| Wiring, broadcast, persistence | `playback.rs` | D2, D9, D12 |
|
||
| Argument validation | `rpc.rs` | D6, the count |
|
||
| Rights matrix | `auth.rs` (existing pinned test) | D1 |
|
||
| Keys and modality | `cbd-tui/src/app/{bindings,sort,mod}.rs` | D14 |
|
||
| Pane feedback | `cbd-tui/src/app/queue.rs` | D15 |
|
||
| Command parsing | `cbd-cli/src/lib.rs` | D17 |
|
||
| Keymap parity | `cbd-web/src/keymap.rs` | D16, D17 |
|
||
|
||
The server rows live under `crabidy-server/src/`.
|
||
|
||
Plus the repository's standing gates: `cargo clippy` clean, `cargo fmt`,
|
||
no new `unwrap` on client-reachable paths, and the book building.
|