# m6p6 — Notification Capping + Adaptive Preferences + Creator Profile Modes + M6 UAT (✅ COMPLETE 2026-02-23) Phase spec and acceptance criteria: [ROADMAP · Milestone 6 · Phase 6](../ROADMAP.md). Milestone index: [README.md](README.md). Backfilled record. ## What shipped 1. **Notification capping** (`tidal/src/db/notification_tracker.rs`). `NotificationCaps { max_per_creator_per_day, max_total_per_day }` attaches to a query via `RetrieveBuilder::notification_caps(caps)` and is enforced as a post-diversity pass, with per-`(user, creator, date)` delivery counts tracked so the cap spans queries rather than only trimming one result page. 2. **Adaptive preference learning rate** (`tidal/src/entities/preference.rs`). The EMA alpha decays logarithmically with a user's update count: ``` alpha = base_alpha / (1 + ln(update_count + 1)) // base_alpha default 0.1 ``` At `count = 0` this is exactly `base_alpha`. Early signals move a cold user's vector hard; later signals refine it gently, which is what keeps a well-established taste profile from being yanked by one outlier interaction. Unit coverage for the decay curve lives beside the implementation (`preference.rs` tests, incl. the explicit `count=0 ⇒ alpha=0.1` case). 3. **Creator profile modes.** `RetrieveBuilder::for_creator(creator_id)` adds the creator filter and restricts candidate generation to that creator's items, so `for_creator(x) + for_you` ranks x's catalogue by the *querying user's* preferences while `for_creator(x) + hot` ranks x's catalogue by heat. 4. **The M6 UAT** (`tidal/tests/m6_uat.rs`) — 9 `#[test]` functions over a shared fixture, exercising the full use-case surface. ## Evidence - `tidal/tests/m6_uat.rs` (9) and `tidal/tests/m6p6_creator_profile.rs` (6). - Recorded at close in the ROADMAP status row: 1,082 total (835 lib + 247 integration), 9 `m6_uat` passing; the same row records that all prior milestone UATs (m2, m3, m4, m5, m5p4) continued to pass — the phase's "no regression" criterion. ## Divergence from the plan **The "update counts are not persisted" limitation is stale — and it was closed by M12, not M7.** `tidal/src/entities/preference.rs:37-45` still carries a `# Known Limitation` block saying `update_counts` is in-memory only, that every user resets to `count = 0` on restart, and that persisting it is "deferred to M7 (Production Hardening)". Both halves are now wrong: - The count **is** persisted. The multi-vector preference checkpoint format serializes it — `[update_count:8 LE][importance_at_anchor:4 LE][anchor_ts:8 LE]` (`tidal/src/entities/multi_preference.rs:127,149`) — with `MultiPreferenceVectors::checkpoint` / `restore` (`multi_preference.rs:642,703`) writing and reading it, and `PreferenceVectors::insert_restored` (`preference.rs:239`) loading a legacy single-vector row back as a cold-start user with its count intact. Round-trip and legacy-row tests exist (`checkpoint_restore_roundtrip_multi_cluster`, `restore_reads_legacy_single_vector_rows_as_cold_start`). - The milestone that closed it was **M12** (multi-vector user preference modelling), not M7. See [milestone-12/multi-vector-preference.md](../milestone-12/multi-vector-preference.md). The stale doc comment is in Rust source and is out of scope for this backfill; recorded here so the next reader of that comment does not re-plan work that is already done.