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

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