You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
a canonical channel has at least one stream variant whose provider channel belongs to the source's activeRevision;
the channel is visible for the selected profile (COALESCE(user_channel_overlays.isHidden, 0) = 0);
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.
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:
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:
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:
never present rows.size as a total when the query is bounded;
never infer hasMore from an unrelated capped query;
a UI may show Показано N without an exact count query;
add an authoritative count only when the product actually needs one;
if count/pagination are added, row/count/window must share the same semantic predicates and deterministic tie-breaks;
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:
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:
exact-head database instrumentation contract on old-edge/current APIs;
row membership after active-revision swap;
hidden-overlay agreement across Playback/Search/Recent/Guide;
no bounded list presented as total and no fake hasMore;
deterministic ordering/tie-break where a window is paged;
zero provider-specific heuristic without a committed fixture;
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.
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:
activeRevision;COALESCE(user_channel_overlays.isHidden, 0) = 0);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/getActiveVariantsuse current source revision; profile-facing row/direct-playback paths enforce hidden-overlay filtering.ChannelSearchDaorevalidates active stream/source truth and profile visibility after FTS candidate selection.EpgGuideDaonow/next membership and programme candidates enforce active catalog revision plus profile visibility.RecentChannelsDaouses the same active/current-revision + non-hidden read rule.CatalogDao.observeActiveCanonicalChannelsis 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:
ChannelSearchSnapshotcarries explicitisTruncated; 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 fromrows.size.Concrete defect found during inventory
The Channels surface used
rows.sizefrom bounded queries as if it were an authoritative total:ChannelQuerydefault 200 / max 500;Активных каналов: N,Избранных каналов: N,Недавно просмотренных каналов: N.That is exactly the false-total/pagination bug class this issue exists to prevent. It is corrected in #107, where
ChannelsRouteis 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:
PlaybackCatalogDao.ChannelSearchDao.RecentChannelsDaoafter feat: add profile-scoped Recent channels v10 #107.EpgGuideDao.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:
rows.sizeas a total when the query is bounded;hasMorefrom an unrelated capped query;Показано Nwithout an exact count query;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
mainand add a focusedActiveChannelTruthContractTest(or equivalent repository-level integration contract). Seed one database with:Assert membership, not presentation order:
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:
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:
hasMore;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.