Skip to content

Catalog: enforce identical truth predicates for rows, counts, pagination and provider pseudo-items #114

Description

@MrFr3di

Context

Derived from the reusable architectural lesson behind StreamVault #2797. This is not a request to copy StreamVault SQL or provider-specific heuristics; it is a MuxTV invariant package required before bounded Guide/pagination work.

Core invariant

For every user-facing channel surface, authoritative membership is defined semantically as:

  1. a canonical channel has at least one stream variant whose provider channel belongs to the source's activeRevision;
  2. the channel is visible for the selected profile (COALESCE(user_channel_overlays.isHidden, 0) = 0);
  3. only then is the surface-specific predicate applied (Favorites, Search query, Recent history, Guide viewport, etc.).

Sorting may differ per surface, but active/profile-visible membership must not drift.

Current inventory

Accepted main already aligns most row projections:

  • PlaybackCatalogDao.observeActiveChannels/findActiveChannel/getActiveVariants use current source revision; profile-facing row/direct-playback paths enforce hidden-overlay filtering.
  • ChannelSearchDao revalidates active stream/source truth and profile visibility after FTS candidate selection.
  • EpgGuideDao now/next membership and programme candidates enforce active catalog revision plus profile visibility.
  • feat: add profile-scoped Recent channels v10 #107 RecentChannelsDao uses the same active/current-revision + non-hidden read rule.
  • CatalogDao.observeActiveCanonicalChannels is intentionally profile-agnostic low-level catalog truth and must not be treated as a profile UI projection.

Search already demonstrates the preferred bounded-result contract: ChannelSearchSnapshot carries explicit isTruncated; Search UI says Показано результатов when truncated and only says Найдено результатов when the snapshot is known not to be truncated. Guide/window work should follow this explicit-state pattern rather than infer completeness from rows.size.

Concrete defect found during inventory

The Channels surface used rows.size from bounded queries as if it were an authoritative total:

  • ChannelQuery default 200 / max 500;
  • feat: add profile-scoped Recent channels v10 #107 Recent default 20 / max 50;
  • UI labels previously said Активных каналов: N, Избранных каналов: N, Недавно просмотренных каналов: N.

That is exactly the false-total/pagination bug class this issue exists to prevent. It is corrected in #107, where ChannelsRoute is already owned: bounded lists now say Показано каналов / избранных / недавних. No duplicate COUNT SQL is introduced just to preserve old wording.

Execution plan

Phase A — semantic inventory and ownership

Keep explicit owners rather than a giant shared SQL string:

  • Playback rows/direct playback: PlaybackCatalogDao.
  • Search candidate/revalidation/summary: ChannelSearchDao.
  • Recent active projection: RecentChannelsDao after feat: add profile-scoped Recent channels v10 #107.
  • Guide now/next and future viewport: EpgGuideDao.
  • profile-agnostic canonical maintenance: CatalogDao.

Document base truth and ensure every profile-facing owner has contract coverage for active revision + hidden overlay.

Phase B — no fake totals / no fake has-more

Rules:

  1. never present rows.size as a total when the query is bounded;
  2. never infer hasMore from an unrelated capped query;
  3. a UI may show Показано N without an exact count query;
  4. add an authoritative count only when the product actually needs one;
  5. if count/pagination are added, row/count/window must share the same semantic predicates and deterministic tie-breaks;
  6. prefer explicit isTruncated/window-completeness state (as Search already does) over deriving completeness from list size.

#107 owns the immediate Channels wording correction. A broader count API is explicitly out of scope there.

Phase C — cross-surface active-truth contract after #107 merge

Create a fresh branch from accepted main and add a focused ActiveChannelTruthContractTest (or equivalent repository-level integration contract). Seed one database with:

  • active revision containing channels A/B;
  • a staged/newer revision not activated;
  • profile-hidden B;
  • favorite overlay on A;
  • Search documents for both active and stale identities;
  • Recent history for active, hidden and temporarily inactive IDs;
  • Guide match/programme fixtures where applicable.

Assert membership, not presentation order:

  • Playback rows expose only active/profile-visible IDs;
  • direct playback cannot resolve hidden/stale IDs;
  • Search result revalidation exposes the same active/profile-visible identities;
  • Recent exposes only active/profile-visible history while retaining bounded hidden/inactive history internally;
  • Guide now/next/viewport never resurrects hidden/stale channels.

Then perform an active-revision swap and repeat the assertions. Avoid duplicating every existing unit test; this contract exists specifically to catch cross-owner drift.

Phase D — Guide row/window/count contract

Before implementing full Guide UI, define the Guide database window as one coherent contract:

  • bounded canonical-channel slice with deterministic stable key/tie-break;
  • bounded time interval plus small prefetch margin;
  • programme overlap predicate defined once for window reads;
  • profile visibility/current source revision applied before surface-specific time projection;
  • explicit window completeness/truncation state when useful;
  • no total derived from viewport rows;
  • if an exact channel count is needed, its predicate is tested against the same seeded truth set.

This phase is the gate for replacing the current Guide placeholder.

Phase E — provider pseudo-items only from evidence

Do not add generic name/hash heuristics for M3U. If a real provider fixture demonstrates structural/pseudo rows, introduce a typed ingestion classification such as:

Playable / StructuralPseudoItem / Malformed(reason)

at the provider/import boundary, not in presentation SQL. Quarantine only evidence-backed structural rows before they enter canonical channel truth.

Acceptance

When self-hosted CI returns, #114 implementation must demonstrate:

  1. exact-head database instrumentation contract on old-edge/current APIs;
  2. row membership after active-revision swap;
  3. hidden-overlay agreement across Playback/Search/Recent/Guide;
  4. no bounded list presented as total and no fake hasMore;
  5. deterministic ordering/tie-break where a window is paged;
  6. zero provider-specific heuristic without a committed fixture;
  7. zero unresolved review threads.

Ordering

Immediate correction lives in #107 because that PR already owns the affected Channels UI. The cross-surface DB contract should start only after #107 is accepted/merged, from fresh main; do not stack a broad #114 implementation on the unverified Room-v10 branch while self-hosted CI is disabled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions