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

4.6 KiB

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:

#[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.