diff --git a/cmd/server/tests/contract_pins_test.go b/cmd/server/tests/contract_pins_test.go index a870d1b7..245ab6c8 100644 --- a/cmd/server/tests/contract_pins_test.go +++ b/cmd/server/tests/contract_pins_test.go @@ -459,6 +459,8 @@ var bookInfoKeys = []string{ "media_item_id", "title", "author", "cover_image_path", "has_conflict", "format_group", "badge", "viewer_percentage", "library_name", + // Episode-collections extras (2.3 widening, every surface emits): + "is_episode", "feed_title", "download_state", "hidden", } func collectionURL(setup *TestServerSetup, id string) string { @@ -500,6 +502,7 @@ func TestContractPin_CollectionsJSON(t *testing.T) { "id", "user_id", "name", "description", "color", "icon", "auto_assign_rules", "view_settings", "books", "book_count", "is_smart", "created_at", "up_next", + "rule_match_mode", // item-2.4 widening (docs/contract_pins.md) } t.Run("user collection with library_id (badged branch)", func(t *testing.T) { diff --git a/docs/contract_pins.md b/docs/contract_pins.md index 804f96e1..1484f766 100644 --- a/docs/contract_pins.md +++ b/docs/contract_pins.md @@ -468,3 +468,16 @@ never-queued episodes) is stamped from the `podcast_queue_seq` SEQUENCE at enqueue time so claim order = enqueue order (the effective listening order). Raw-row pins (TestContractPin_MediaListBothBranches → rawRowKeys) whitelist the key. Nothing removed, nothing renamed. + +## 2026-10-30 — episode collections (additive widening, plan item 2/2.3/2.4) + +Two shapes widen, both ADDITIVE — nothing removed, nothing renamed: + +- **Collections envelope** (`GET /api/collections/:id` → envelopeKeys) + gains `rule_match_mode` (smart-collection match semantics, 'any' + legacy default | 'all'). Static collections emit the default value. +- **BookInfo** (every card surface → bookInfoKeys) gains four STABLE + keys — `is_episode`, `feed_title`, `download_state`, `hidden` — the + episode-collections extras. Adapters that predate episode awareness + emit their zero values (`false`/`""`), matching the + viewer_percentage precedent (nil-representable, key always present). diff --git a/internal/handlers/cards.go b/internal/handlers/cards.go index b20ac44a..4ceeebba 100644 --- a/internal/handlers/cards.go +++ b/internal/handlers/cards.go @@ -50,11 +50,14 @@ type BookInfo struct { // LEFT JOIN missed under RLS (unsubscribed episode, or a // family-lens hidden book) — the row renders as an "unavailable" // hint instead of silently vanishing. Adapters that don't load - // visibility leave Hidden false (cards stay fully rendered). - IsEpisode bool `json:"is_episode,omitempty"` - FeedTitle string `json:"feed_title,omitempty"` - DownloadState string `json:"download_state,omitempty"` - Hidden bool `json:"hidden,omitempty"` + // visibility leave Hidden false (cards stay fully rendered). The + // four keys are NOT omitempty: every BookInfo surface emits the + // same stable key set (the viewer_percentage precedent — + // contract-pin widening, docs/contract_pins.md 2026-10-30). + IsEpisode bool `json:"is_episode"` + FeedTitle string `json:"feed_title"` + DownloadState string `json:"download_state"` + Hidden bool `json:"hidden"` } // LibraryTypeMap builds libraryID→library-type-name for badge resolution