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) |