7.0 KiB
Spec: Feedback Loop UX
Overview
tidalDB already has the backend machinery for processing user feedback signals: signal_with_context records like/hide/dislike/block/skip events, updates the HardNegIndex for negative signals, updates UserStateIndex for seen/hidden/blocked state, adjusts preference vectors for positive engagement, and records to cohort ledgers. The RETRIEVE and SEARCH executors exclude hard negatives and hidden items in Stage 2.5 of the query pipeline.
What is missing is a coherent, single-call feedback API that a client application can invoke when a user taps mute, hide, like, or dislike on a piece of content — and a contract guaranteeing that the next query for that user reflects the feedback immediately, within the same process, without waiting for WAL flush, background sync, or index rebuild.
This feature closes that gap by introducing a FeedbackAction enum and a db.submit_feedback() method that atomically applies all side effects for a feedback action, and by documenting and testing the "immediate reflection" guarantee.
Problem Statement
Today, a client application that wants to implement "hide this item" must:
- Call
signal_with_context("hide", item_id, 1.0, now, Some(user_id), creator_id)to record the signal and update the hard-neg bitmap. - Optionally call
user_state.add_hide(user_id, item_id)if it has access to the internal index (it does not — this ispub(crate)). - Hope that the next
retrieve()orsearch()call happens after the DashMap write is visible.
Problems with this approach:
- No mute concept. "Mute creator" requires calling
signal_with_context("block", ...)AND updatingUserStateIndex::add_block_creator. The second call ispub(crate)— not exposed to the public API. - Signal type confusion. The client must know that "hide" maps to
HardNegIndex, "block" maps toBlockedState, and "like" maps to preference vector updates. This is implementation leakage. - No undo. Undoing a hide requires calling
hard_negatives.remove()anduser_state.remove_hide(), both of which are internal. There is no public undo path. - No reflection guarantee documented. The in-memory bitmap updates are synchronous within
signal_with_context, but this is not documented or tested as a contract.
Goals
- Introduce a
FeedbackActionenum with variants:Like,Unlike,Hide,Unhide,MuteCreator,UnmuteCreator,Dislike,UndoDislike,Save,Unsave,NotInterested. - Expose
TidalDb::submit_feedback(user_id, entity_id, action, timestamp)as the single public entry point for all user feedback. - Each action atomically performs all required side effects (signal write, hard-neg update, user-state update, preference vector update) in one call.
- Guarantee that any
retrieve()orsearch()call withfor_userissued aftersubmit_feedback()returnsOk(())will reflect the feedback (item excluded, creator items excluded, preference adjusted). - Support undo for all reversible actions (unlike, unhide, unmute, undo dislike, unsave).
- Expose
TidalDb::feedback_state(user_id, entity_id) -> FeedbackStatefor reading the current feedback state of an item for a user (is it liked? hidden? is the creator muted?), so the UI can render toggle states. - Write integration tests that verify the "immediate next-query reflection" contract for every action.
Non-Goals
- This feature does not add a new HTTP/REST endpoint to
tidal-server. The server router can expose this via aPOST /feedbackroute in a future feature. - This feature does not add rate limiting to feedback actions. The existing session rate limiter and signal backpressure mechanisms remain unchanged.
- This feature does not add batch feedback (e.g., "hide all items from this category"). Each call is for one user-entity pair.
- This feature does not persist undo history. Undo is modeled as recording the inverse action, not as a revertible log.
- This feature does not add animation or debounce logic — that is a client-side concern.
- This feature does not change the signal schema or introduce new signal types. It uses the existing "like", "hide", "dislike", "block", "skip" signal types already declared in schemas.
Behavioral Specification
FeedbackAction Enum
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FeedbackAction {
Like,
Unlike,
Hide,
Unhide,
MuteCreator { creator_id: u64 },
UnmuteCreator { creator_id: u64 },
Dislike,
UndoDislike,
Save,
Unsave,
NotInterested,
}
submit_feedback Method
impl TidalDb {
pub fn submit_feedback(
&self,
user_id: u64,
entity_id: EntityId,
action: FeedbackAction,
timestamp: Timestamp,
) -> crate::Result<()>;
}
Action Dispatch Table
| Action | Signal Written | Hard-Neg | UserState Update | Preference Vector |
|---|---|---|---|---|
Like |
"like" weight=1.0 |
-- | add_like(user, item) |
Positive blend |
Unlike |
-- | -- | Remove from liked bitmap | -- |
Hide |
"hide" weight=1.0 |
add(user, item) |
add_hide(user, item) |
-- |
Unhide |
"hide" weight=-1.0 |
remove(user, item) |
remove_hide(user, item) |
-- |
MuteCreator |
"block" weight=1.0 |
-- | add_block_creator(user, creator) |
-- |
UnmuteCreator |
-- | -- | remove_block_creator(user, creator) |
-- |
Dislike |
"dislike" weight=1.0 |
add(user, item) |
mark_seen(user, item) |
Negative blend (0.3x damping) |
UndoDislike |
-- | remove(user, item) |
-- | -- |
Save |
-- | -- | add_save(user, item) |
-- |
Unsave |
-- | -- | Remove from saved bitmap | -- |
NotInterested |
"skip" weight=1.0 |
-- (unless 3+ skips) | mark_seen(user, item) |
Mild negative (0.1x damping) |
FeedbackState
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FeedbackState {
pub is_liked: bool,
pub is_hidden: bool,
pub is_disliked: bool,
pub is_saved: bool,
pub is_creator_muted: bool,
}
Immediate Reflection Guarantee
After submit_feedback returns Ok(()), any subsequent retrieve() or search() call with for_user set to the same user_id will reflect the feedback. This works because DashMap insertions are synchronous and the query pipeline reads from the same DashMap instances.
Acceptance Criteria
- Like/Unlike roundtrip toggles liked bitmap correctly.
- Hide/Unhide roundtrip toggles hard-neg and hidden-items correctly.
- MuteCreator/UnmuteCreator roundtrip toggles blocked-creators correctly.
- Dislike/UndoDislike roundtrip toggles hard-neg correctly.
- Save/Unsave roundtrip toggles saved bitmap correctly.
- 3 NotInterested actions escalate to hard negative.
- feedback_state returns correct booleans for all states.
- All signal-writing actions are WAL-durable.
- Undo actions on untouched items are no-ops (no panic).
- 21 unit tests and 10 integration tests pass.
- cargo fmt and cargo clippy clean (no warnings from feedback module).
- Full lib test suite (1345 tests) passes with no regressions.