docs(traces): tag the twelve "Done but untraced" requirements, and stop the matrix over-reporting

Twelve requirements were marked Done in docs/requirements.md with zero TRACES
anywhere in the tree. The features work — the tags were simply never written —
so the matrix over-reported on exactly the requirements a reviewer would most
want to verify. Each is now tagged at the code that actually implements it:

- JA-006 / JA-009 / JA-013 / JA-014 / JA-015 / JA-018 and IR-022 / IR-024 at
  their Jellyfin call sites in repository/online.rs (search, get_item's
  MediaStreams/People fields, Items/Resume, Shows/NextUp, FavoriteItems DELETE,
  get_person/get_items_by_person), plus the commands that expose them.
- UR-006 / IR-006 across the lockscreen spine: JellyTauPlaybackService (the
  MediaSessionCompat owner), the nativeOnMediaCommand JNI intake, and
  LockscreenMetadata / update_lockscreen_metadata.
- IR-008 at both audio-focus mechanisms — ExoPlayer-managed for audio, the
  manual AudioFocusRequest listener for video — and at the media-type string
  that chooses between them.
- UR-037 (with DR-042, also untraced) on the video-library poster grid:
  LibraryGrid, MediaCard, and the tv/movies routes.

Resolve contradictory statuses across layers, evidence first:

- IR-018/IR-019 were Planned under Done URs because they were scoped to libmpv.
  MpvBackend is the audio-only backend and overrides neither
  set_subtitle_track nor set_audio_track — the trait's not_implemented()
  default still stands — so UR-020/UR-021 are met by ExoPlayer and by the
  HTML5 <video> path instead. Both IRs are re-scoped to those backends and
  marked Done; IT-008/IT-009 and the stale @req-planned markers in backend.rs
  follow.
- IR-005 (MPRIS) stays Planned: there is no MPRIS/D-Bus code or dependency in
  the project and update_lockscreen_metadata is a no-op off Android. UR-006 is
  corrected to Done (Android) rather than the IR being marked Done.
- A note under the IR table records where a UR is met by a different mechanism
  than its IR anticipated.

Define the two dangling IDs the source already referenced: DR-189 (the control
bar never auto-hid on a touchscreen, because its timer was armed only from
onmousemove) and UT-188 (its rule test). The live-denominator assertion in
extract-traces.test.ts moves 187/330 to 188/331 accordingly.

Traced requirements 444 to 459; IR coverage 19/32 to 25/32.
This commit is contained in:
2026-08-16 22:58:55 +02:00
parent 73641e192c
commit ebf9a99b80
16 changed files with 5059 additions and 4512 deletions
+60 -2
View File
@@ -760,7 +760,11 @@ struct JellyfinItem {
/// `UserData` in the `Fields=` list so the shape is explicit rather than
/// dependent on the server's default field set.
///
/// TRACES: UR-069 | DR-113, JA-034 | UT-099
/// `PlaybackPositionTicks` is the server's resume position for the item, and the
/// only place it is published — Jellyfin has no per-item "resume position"
/// endpoint, so reading `UserData` *is* how a resume point is obtained.
///
/// TRACES: UR-019, UR-069 | DR-113, JA-013, JA-034 | UT-099
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
struct JellyfinUserData {
@@ -854,6 +858,8 @@ fn build_get_items_endpoint(
///
/// Pulled out of `get_latest_items` so the query can be asserted without an
/// HTTP server, matching `build_favorites_endpoint`.
///
/// TRACES: UR-024, UR-034 | IR-024, JA-016
fn build_latest_items_endpoint(user_id: &str, parent_id: &str, limit: Option<usize>) -> String {
format!(
"/Users/{}/Items/Latest?ParentId={}&Limit={}&GroupItems=true&Fields=BackdropImageTags,ParentBackdropImageTags,UserData",
@@ -875,7 +881,7 @@ fn build_latest_items_endpoint(user_id: &str, parent_id: &str, limit: Option<usi
/// Pulled out of `get_next_up_episodes` so the query can be asserted without an
/// HTTP server, matching `build_favorites_endpoint`.
///
/// TRACES: UR-059 | DR-197, JA-036 | UT-190, UT-191
/// TRACES: UR-023, UR-059 | DR-197, JA-014, JA-036 | UT-190, UT-191
fn build_next_up_endpoint(user_id: &str, series_id: Option<&str>, limit: Option<usize>) -> String {
let mut endpoint = format!(
"/Shows/NextUp?UserId={}&Limit={}&EnableResumable=false&Fields=BackdropImageTags,ParentBackdropImageTags,UserData",
@@ -1129,6 +1135,17 @@ impl MediaRepository for OnlineRepository {
})
}
/// Fetch one item with every field the detail and player screens need.
///
/// The `Fields=` list is the load-bearing part: Jellyfin omits these unless
/// they are named. `MediaStreams` is what makes the item's **audio and
/// subtitle tracks** knowable at all — there is no separate "tracks"
/// endpoint, so this single call is how the player learns which audio tracks
/// an item offers (`to_media_item` maps them, and the player's selector
/// filters them by `kind`). `People` is likewise how **cast and crew** are
/// obtained.
///
/// TRACES: UR-021, UR-035 | IR-016, IR-022, JA-005, JA-009
async fn get_item(&self, item_id: &str) -> Result<MediaItem, RepoError> {
let endpoint = format!("/Users/{}/Items/{}?Fields=BackdropImageTags,ParentBackdropImageTags,People,MediaStreams,MediaSources,PremiereDate,UserData", self.user_id, item_id);
@@ -1152,6 +1169,14 @@ impl MediaRepository for OnlineRepository {
.collect())
}
/// Continue Watching: the items this user has started and not finished.
///
/// `/Users/{uid}/Items/Resume` is the server-side answer to both "what goes
/// in the Continue Watching row" and "where was this left off" — each item
/// carries its own `UserData.PlaybackPositionTicks`, which is why `UserData`
/// is named in `Fields=` rather than left to the server's default field set.
///
/// TRACES: UR-019, UR-023 | IR-024, JA-013, JA-015
async fn get_resume_items(
&self,
parent_id: Option<&str>,
@@ -1175,6 +1200,10 @@ impl MediaRepository for OnlineRepository {
.collect())
}
/// "Next Up": the episode that follows the ones this user has finished,
/// per series — the Shows-scoped counterpart to Continue Watching.
///
/// TRACES: UR-023, UR-059 | IR-024, JA-014
async fn get_next_up_episodes(
&self,
series_id: Option<&str>,
@@ -1348,6 +1377,11 @@ impl MediaRepository for OnlineRepository {
.collect())
}
/// Continue Watching, narrowed to movies — the home screen's movie row and
/// the movie library's own hero both want the unfinished films without the
/// episodes mixed in.
///
/// TRACES: UR-019, UR-034 | IR-024, JA-013, JA-015
async fn get_resume_movies(&self, limit: Option<usize>) -> Result<Vec<MediaItem>, RepoError> {
let limit_str = limit.unwrap_or(16);
let endpoint = format!(
@@ -1423,6 +1457,14 @@ impl MediaRepository for OnlineRepository {
Ok(genres)
}
/// Search every library the user can see.
///
/// `Recursive=true` with no `ParentId` is what makes this cross-library
/// rather than folder-scoped; a caller narrowing the search passes the item
/// types through `SearchOptions` (already expanded from an opaque
/// `SearchScope` on this side of the boundary).
///
/// TRACES: UR-008 | IR-010, JA-006
async fn search(
&self,
query: &str,
@@ -2195,6 +2237,12 @@ impl MediaRepository for OnlineRepository {
})
}
/// Un-favourite an item: the same `/Users/{uid}/FavoriteItems/{id}` resource
/// as [`Self::mark_favorite`], removed rather than posted. Written out by
/// hand rather than through `post_json` because it is the one favourite call
/// that needs `DELETE`.
///
/// TRACES: UR-017 | JA-018, DR-021
async fn unmark_favorite(&self, item_id: &str) -> Result<(), RepoError> {
let endpoint = format!("/Users/{}/FavoriteItems/{}", self.user_id, item_id);
let url = format!("{}{}", self.server_url, endpoint);
@@ -2316,12 +2364,22 @@ impl MediaRepository for OnlineRepository {
result
}
/// A single Person item (actor, director, …) by id.
///
/// Jellyfin models people as ordinary items, so this is the plain item
/// endpoint rather than anything under `/Persons`; the cast entries returned
/// on an item's `People` field carry the ids this is called with.
///
/// TRACES: UR-035, UR-036 | IR-022, JA-030
async fn get_person(&self, person_id: &str) -> Result<MediaItem, RepoError> {
let endpoint = format!("/Users/{}/Items/{}", self.user_id, person_id);
let item: JellyfinItem = self.get_json(&endpoint).await?;
Ok(item.to_media_item(self.user_id.clone()))
}
/// A person's filmography — every item they are credited on.
///
/// TRACES: UR-036 | IR-022, JA-031
async fn get_items_by_person(
&self,
person_id: &str,