tidaldb/.sdlc/features/p1-briefing-ux-reason-labels/design.md

120 lines
4.6 KiB
Markdown

# Design: Briefing UX & Reason Labels
## Overview
This feature adds a `ReasonLabel` system to tidalDB's query pipeline. Each ranked result carries structured reason codes explaining why it was surfaced and what factors dominated its score. The design integrates into the existing scoring pipeline with minimal overhead by tagging candidates at each stage rather than computing reasons post-hoc.
## Architecture
### New Types
```
tidal/src/ranking/reason.rs (NEW)
```
This module defines the core types:
```rust
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ReasonCode {
FollowedCreator, TrendingGlobal, TrendingInCategory, TrendingInCohort,
PreferenceMatch, SocialProof, HighQuality, HiddenGem, NewFromFollowed,
Rising, Controversial, ExplorationBudget, TopInWindow, SemanticMatch,
TextRelevance, CoEngagement, SessionContext, CohortPopular, SavedSearchMatch,
}
pub struct ReasonLabel {
pub code: ReasonCode,
pub context: HashMap<String, String>,
pub weight: f64,
}
```
### Integration Points
#### 1. `ScoredCandidate` (existing type, modified)
Add a `reasons: Vec<ReasonLabel>` field to `ScoredCandidate` in `ranking/executor/context.rs`.
#### 2. `RetrieveResult` and `SearchResultItem` (existing types, modified)
Both gain a `pub reasons: Vec<ReasonLabel>` field. This is the public API surface.
#### 3. Pipeline Integration
```
RETRIEVE Pipeline:
Stage 1 (Candidate Gen) --> tag source strategy reasons
Stage 2 (Filters) --> no reason changes
Stage 3 (Scoring) --> tag dominant signal reasons + populate signal_snapshot
Stage 4 (Diversity) --> no reason changes (reasons travel with candidates)
Stage 5 (Assembly) --> select top reasons, attach to results
SEARCH Pipeline:
Stage 0 (Scope) --> tag scope reasons (TrendingGlobal, etc.)
Stage 1 (BM25+ANN) --> tag retrieval method (TextRelevance, SemanticMatch)
Stage 2 (Filters) --> no reason changes
Stage 3 (Scoring) --> tag dominant signal reasons + populate signal_snapshot
Stage 4 (Diversity) --> no reason changes
Stage 5 (Assembly) --> select top reasons, attach to results
```
### Detailed Stage Logic
#### Stage 1: Candidate Generation Reasons
| Candidate Strategy | Reason Code | Context Fields |
|---|---|---|
| `Relationship` (following) | `FollowedCreator` | `creator_id` |
| `SignalRanked` with trending profile | `TrendingGlobal` | -- |
| `SignalRanked` with category filter | `TrendingInCategory` | `category` |
| `SignalRanked` with cohort | `TrendingInCohort` | `cohort` |
| `Scan` (default) | (no reason from stage 1) | -- |
| `EmbeddingSimilarity` | `PreferenceMatch` | -- |
#### Stage 3: Signal Scoring Reasons
In `ProfileExecutor::score()`, after scoring each candidate:
1. **Populate `signal_snapshot`**: Record boost signal values.
2. **Tag dominant reason**: Map sort mode to reason code, detect social proof and quality gates.
3. **Merge with Stage 1 reasons**: Append to candidate's existing reason list.
#### Stage 5: Assembly / Reason Selection
1. Sort by weight descending.
2. Apply dominance threshold (drop < 0.10).
3. Take top 3.
4. Attach to result.
### Performance Budget
- Memory: ~72 KB for 200 candidates at 3 reasons each. Negligible.
- CPU: O(n * k) where n = candidate count, k bounded by ~5.
- No additional signal reads: reasons derived from already-computed values.
## File Changes
| File | Change |
|---|---|
| `tidal/src/ranking/reason.rs` | **NEW** -- `ReasonCode`, `ReasonLabel`, threshold constants |
| `tidal/src/ranking/mod.rs` | Add `pub mod reason;` and re-export types |
| `tidal/src/ranking/executor/context.rs` | Add `reasons: Vec<ReasonLabel>` to `ScoredCandidate` |
| `tidal/src/ranking/executor/mod.rs` | Populate `signal_snapshot` and tag scoring reasons |
| `tidal/src/query/executor/pipeline.rs` | Tag candidate-gen reasons, select top reasons in assembly |
| `tidal/src/query/search/executor/pipeline.rs` | Tag search-method reasons, select top reasons in assembly |
| `tidal/src/query/retrieve/types.rs` | Add `reasons: Vec<ReasonLabel>` to `RetrieveResult` |
| `tidal/src/query/search/types.rs` | Add `reasons: Vec<ReasonLabel>` to `SearchResultItem` |
| `tidal/src/lib.rs` | Re-export `ReasonCode`, `ReasonLabel` |
| `tidal/tests/p1_reason_labels.rs` | **NEW** -- integration tests |
## Alternatives Considered
### Post-hoc reason inference
Rejected: Cannot distinguish candidate source strategies or exploration-injected items.
### String-based reason codes
Rejected: No compile-time exhaustiveness checking, risk of typos.
### Separate "explain" query mode
Rejected: Overhead is negligible (< 5%), always-on is simpler.