tidaldb/.sdlc/features/pg1-personalization-correctness/design.md

5.2 KiB

Design: Personalization Correctness Verification

Overview

This feature adds a comprehensive integration test suite (tidal/tests/pg1_personalization_correctness.rs) that mathematically verifies every link in the personalization loop. No production code changes are required — this is a pure verification feature.

Architecture

pg1_personalization_correctness.rs
├── mod decay_tests
│   ├── single_event_decay_matches_analytical
│   ├── multi_event_decay_matches_analytical
│   ├── out_of_order_converges_to_in_order
│   └── decay_score_is_zero_after_many_half_lives
├── mod windowed_count_tests
│   ├── one_hour_window_exact_count
│   ├── velocity_equals_count_over_duration
│   └── all_time_count_is_monotonic
├── mod preference_vector_tests
│   ├── ema_update_matches_manual_computation
│   ├── adaptive_learning_rate_decays
│   └── dimension_mismatch_is_noop
├── mod ranking_reactivity_tests
│   ├── signal_immediately_visible_in_retrieve
│   └── signal_immediately_visible_in_search
├── mod score_ordering_tests
│   ├── more_engagement_ranks_higher
│   ├── interaction_boost_is_additive
│   ├── boost_increases_score_linearly
│   ├── penalty_decreases_score_linearly
│   └── no_nan_or_infinity_in_scores
├── mod gate_tests
│   ├── below_threshold_excluded
│   └── at_threshold_included
└── mod diversity_tests
    └── max_per_creator_enforced

Test Schema

All tests share a common schema builder helper:

fn test_schema() -> Schema {
    let mut b = SchemaBuilder::new();
    let _ = b.signal("view", EntityKind::Item,
        DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600) })
        .windows(&[Window::OneHour, Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();
    let _ = b.signal("like", EntityKind::Item,
        DecaySpec::Exponential { half_life: Duration::from_secs(14 * 24 * 3600) })
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    let _ = b.signal("share", EntityKind::Item,
        DecaySpec::Exponential { half_life: Duration::from_secs(3 * 24 * 3600) })
        .windows(&[Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();
    let _ = b.signal("completion", EntityKind::Item,
        DecaySpec::Exponential { half_life: Duration::from_secs(30 * 24 * 3600) })
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();
    b.build().unwrap()
}

Analytical Reference Implementation

The test file includes a pure-Rust analytical decay calculator used as the oracle:

/// Compute expected decay score analytically.
/// events: Vec<(timestamp_ns, weight)>, sorted chronologically.
/// query_time_ns: the time at which the score is evaluated.
/// lambda: decay rate (ln(2) / half_life_secs).
fn analytical_decay_score(events: &[(u64, f64)], query_time_ns: u64, lambda: f64) -> f64 {
    events.iter().fold(0.0, |acc, &(ts, w)| {
        let dt = (query_time_ns.saturating_sub(ts)) as f64 / 1e9;
        acc + w * (-lambda * dt).exp()
    })
}

This differs from the incremental implementation (HotSignalState::on_signal + current_score) by computing from first principles — the two must agree within 1e-10 relative error.

Reactivity Test Design

1. Open ephemeral TidalDb with test schema + "for_you" profile
2. Write 10 items with metadata
3. Record view signals for items 1-5 (established baseline)
4. let t0 = Instant::now()
5. db.signal("like", item_1, 10.0, Timestamp::now())  // big boost
6. let results = db.retrieve(&Retrieve::builder()
       .profile("for_you")
       .for_user(user_1)
       .limit(10)
       .build())
7. assert item_1 is in top 3
8. assert t0.elapsed() < Duration::from_millis(100)

The 100ms budget is generous — in-memory path is typically < 1ms. The budget accounts for worst-case WAL append and any scheduling jitter.

Score Ordering Test Design

For monotonicity verification:

  1. Create two items A and B with identical metadata.
  2. Record 10 views for A, 5 views for B.
  3. Retrieve with for_you profile.
  4. Assert A ranks above B (more views = higher score when all else is equal).

For interaction boost:

  1. Create items from creator_1 and creator_2.
  2. Record interaction between user and creator_1.
  3. Retrieve for user — items from creator_1 should rank higher than equivalent items from creator_2.

Gate Test Design

  1. Create profile with gate: { signal: "view", agg: Value, window: AllTime, min_threshold: 5.0 }.
  2. Write items: A with 10 views, B with 3 views, C with 5 views.
  3. Retrieve — A and C should appear, B should not.

Diversity Test Design

  1. Create 25 items across 5 creators with varying engagement levels.
  2. Use trending builtin profile which has max_per_creator=1.
  3. Retrieve with limit 10.
  4. Assert diversity is enforced: max per creator <= 5, at least 2 creators represented.

Files Changed

File Change
tidal/tests/pg1_personalization_correctness.rs New integration test file (all tests)

No Production Code Changes

This feature is purely additive — new test file only. No changes to tidal/src/ are required.