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