tidaldb/.sdlc/features/p1-feedback-loop-ux/design.md

3.3 KiB

Design: Feedback Loop UX

Summary

One new file (db/feedback.rs) with FeedbackAction enum, FeedbackState struct, submit_feedback(), feedback_state(), and try_negative_preference_update(). Two new methods on UserStateIndex (remove_like, remove_save). One new field on TidalDb (skip_counter). Re-exports in lib.rs. No WAL format changes. No schema changes. No new dependencies.

Module Change Map

db/
  feedback.rs       <- NEW: FeedbackAction, FeedbackState, submit_feedback(), feedback_state()
  mod.rs            <- skip_counter field on TidalDb, pub mod feedback declaration
  from_parts.rs     <- skip_counter initialization in from_parts()

entities/
  user_state.rs     <- add remove_like(), remove_save() methods

lib.rs              <- pub use FeedbackAction, FeedbackState

Data Structures

FeedbackAction Enum (Copy, 11 variants)

All variants are data-free or contain only u64, making the enum Copy-able. Passed by value to submit_feedback.

FeedbackState Struct (5 bools)

Read-only state derived from in-memory indexes. No I/O. #[allow(clippy::struct_excessive_bools)] applied since these are genuine independent boolean states for UI toggle rendering.

Skip Escalation Counter

skip_counter: DashMap<(u64, u32), u8> on TidalDb. Incremented on each NotInterested action. At threshold (3), promotes to hard negative. Ephemeral (resets on restart).

Method Implementations

submit_feedback Dispatch

submit_feedback(user_id, entity_id, action, timestamp)
  |-- require_writeable("submit_feedback")
  |-- match action:
  |   Like:           signal("like") + add_like + mark_seen + try_update_preference_vector
  |   Unlike:         remove_like
  |   Hide:           signal("hide") + hard_neg.add + add_hide + mark_seen
  |   Unhide:         signal("hide", -1.0) + hard_neg.remove + remove_hide
  |   MuteCreator:    signal("block") + add_block_creator
  |   UnmuteCreator:  remove_block_creator
  |   Dislike:        signal("dislike") + hard_neg.add + mark_seen + try_negative_preference_update(0.3)
  |   UndoDislike:    hard_neg.remove
  |   Save:           add_save
  |   Unsave:         remove_save
  |   NotInterested:  signal("skip") + mark_seen + try_negative_preference_update(0.1) + skip escalation
  |-- Ok(())

feedback_state Implementation

Reads from user_state and hard_negatives indexes. Distinguishes Hide (hidden_items + hard_neg) from Dislike (hard_neg only) by checking hidden_items bitmap presence.

try_negative_preference_update

Reads item embedding, negates it, blends with reduced learning rate via PreferenceVectors::update_with_custom_rate. Uses let...else patterns for early returns on missing storage/embedding.

Immediate Reflection Guarantee

DashMap insertions on the write side complete before submit_feedback returns Ok(()). DashMap reads on the query side in Stage 2.5 see the latest values. No cache, no eventual consistency, no batch buffer between write and read within the same process.

Performance Characteristics

Operation Cost
submit_feedback (Like) 1 signal write (~2us) + 2 DashMap inserts (~100ns each) + preference lookup (~500ns)
submit_feedback (Unlike) 1 DashMap removal (~100ns)
feedback_state 4 DashMap reads (~400ns total, no I/O)
Skip escalation check 1 DashMap entry + u8 compare (~100ns)