76 lines
3.3 KiB
Markdown
76 lines
3.3 KiB
Markdown
# 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) | |