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

5.8 KiB

Personalization Correctness Verification

Problem Statement

tidalDB's core value proposition is the personalization loop: a signal is written (view, like, share, completion), its effect decays over time, and subsequent ranking queries reflect the updated signal state within milliseconds. If any link in this chain is mathematically wrong or stale, the entire product thesis falls apart.

Today the loop is implemented and exercised by milestone UAT tests, but there is no dedicated verification suite that:

  1. Proves the mathematical correctness of exponential decay (hot tier), windowed counting (warm tier), velocity computation, and preference vector EMA updates against known analytical solutions.
  2. Proves ranking reactivity — that a signal write is reflected in the next ranking query within a bounded latency (target: < 100ms end-to-end from signal() return to retrieve() observing the change).
  3. Proves score monotonicity and ordering invariants — that the ranking pipeline preserves the expected ordering when signal weights, decay parameters, and boost configurations change.

Scope

In Scope

  • Decay correctness tests: Given a known signal history (timestamps, weights, lambda), assert that HotSignalState::current_score() matches the analytical closed-form S(t) = sum_i(w_i * exp(-lambda * (t - t_i))) within IEEE 754 f64 tolerance (< 1e-10 relative error).
  • Out-of-order event correctness: Verify that out-of-order signal ingestion produces the same final score as in-order ingestion (modulo f64 ordering differences bounded by < 1e-10).
  • Windowed count correctness: Verify that BucketedCounter returns accurate counts for 1h window (minute-bucket granularity), and that 24h/7d windows degrade predictably for sparse event streams (documented in MEMORY.md).
  • Velocity correctness: Verify that read_velocity() returns windowed_count / window_duration_seconds for non-AllTime windows and 0.0 for AllTime.
  • Preference vector EMA correctness: Verify that PreferenceVectors::update() computes the EMA correctly: v_new = (1 - alpha) * v_old + alpha * embedding, with adaptive learning rate alpha = base / (1 + ln(count + 1)).
  • Ranking reactivity test: Write a signal via db.signal(), immediately call db.retrieve() with a profile that uses the signal, and assert the signal is reflected — total wall-clock time < 100ms.
  • Score ordering invariants: Verify that for the for_you profile, higher engagement (more likes, more views) produces higher ranking scores. Verify that the interaction boost (INTERACTION_BOOST_WEIGHT = 0.3) is additive and monotonic.
  • Boost/penalty linearity: Verify that profile boosts are additive (score += weight * agg) and penalties are subtractive (score -= weight * agg), and that neither can produce NaN or infinity.
  • Gate filtering correctness: Verify that candidates below a gate threshold are excluded, and candidates at or above the threshold pass.
  • Diversity post-condition: Verify that after diversity enforcement, no creator exceeds max_per_creator items in the result set.

Out of Scope

  • Baseline comparison (covered by pg1-baseline-comparison).
  • Metrics instrumentation (covered by pg1-instrumented-metrics).
  • Distributed/replicated correctness (covered by M8 CRDT reconciliation tests).
  • Community policy enforcement (covered by M10 tests).
  • Performance benchmarking beyond the 100ms reactivity assertion.

Success Criteria

Criterion Measurement Target
Decay accuracy Relative error vs. analytical closed-form < 1e-10
Out-of-order convergence Final score difference (in-order vs out-of-order) < 1e-10
Windowed count accuracy (1h) Exact match for minute-granularity events 0 error
Velocity formula Exact match: count / window_seconds 0 error
Preference EMA Relative error vs. manual EMA computation < 1e-6 (f32)
Ranking reactivity Wall-clock from signal() return to retrieve() observing change < 100ms
Score monotonicity Higher engagement => higher score for for_you profile Boolean pass
Boost/penalty linearity Additive/subtractive; no NaN/Inf Boolean pass
Gate filtering Below-threshold excluded, at-threshold included Boolean pass
Diversity post-condition max_per_creator never exceeded Boolean pass

Technical Approach

All verification tests will be placed in a new integration test file tidal/tests/pg1_personalization_correctness.rs. Tests use TidalDb::builder().ephemeral() with a controlled schema to avoid filesystem dependencies.

Decay Verification Strategy

The analytical reference is computed independently in the test using the recurrence:

S(0) = 0
For each event (t_i, w_i) in chronological order:
  S(t_i) = S(t_{i-1}) * exp(-lambda * (t_i - t_{i-1})) + w_i
At query time t_q:
  S(t_q) = S(t_last) * exp(-lambda * (t_q - t_last))

This is compared against db.read_decay_score() which internally calls HotSignalState::current_score().

Reactivity Verification Strategy

A tight loop: write signal, then immediately retrieve. Measure wall-clock time. The signal write path is synchronous (WAL-first, then in-memory hot/warm update), so the score is available on the next read without any async propagation delay. The 100ms budget includes WAL append, hot-tier CAS, and full retrieve pipeline execution.

Dependencies

None. This feature uses only existing public API methods.

Risks

Risk Mitigation
f64 precision differences across platforms Use relative error bounds (1e-10) rather than exact equality
Warm-tier 24h/7d inaccuracy for sparse streams Document as known limitation; test only 1h window for exact correctness
Test flakiness from wall-clock timing Use Instant::elapsed() with generous margin (100ms is 10x expected latency)