# m6p2 — Social Graph Extension + Collaborative Filtering (✅ COMPLETE 2026-02-23) Phase spec and acceptance criteria: [ROADMAP · Milestone 6 · Phase 2](../ROADMAP.md). Milestone index: [README.md](README.md). Backfilled record. ## What shipped 1. **Reverse relationship index.** Given a creator, retrieve their inbound follower set (`RoaringBitmap`-backed), maintained on every relationship write and persisted so it survives restart. 2. **`FilterExpr::social_graph(user_id, depth)`** (`tidal/src/query/executor/social_filter.rs`, `tidal/src/storage/indexes/filter/expr.rs`). Depth 1 constrains candidates to items from followed creators; depth 2 expands through the resolved follow graph. When combined with a trending profile, velocity reads are scoped to the resolved subgraph (`tidal/src/ranking/executor/scoring.rs`). 3. **Co-engagement index** (`tidal/src/entities/co_engagement.rs`). On a positive engagement (like, or completion ≥ 0.8) pairwise edges are recorded between the engaged item and the user's last `USER_RECENT_CAPACITY = 50` positively engaged items; edge weight increments per co-occurrence. Edges are **asymmetric** — `(A,B)` and `(B,A)` are separate entries, and scoring keys on `(seed_item, candidate)`. Bounded at `DEFAULT_CO_ENGAGEMENT_CAPACITY = 50_000` pairs. 4. **Collaborative-filtering boost in `related`.** The ranking executor takes an optional `CoEngagementIndex` (`with_co_engagement`, `tidal/src/ranking/executor/mod.rs:212`) and folds a co-engagement term into the score for `related`-style queries, with a `co_engagement` entry in the reason snapshot so the contribution is explainable. ## Evidence - `tidal/tests/m6_social.rs` — 8 `#[test]` functions. - `tidal/benches/social.rs` — social-graph benchmarks added by the same commit; numbers recorded in [docs/profiling/social-scale.md](../../profiling/social-scale.md). - `tidal/tests/m7p3_social_scale.rs` — M7 re-tests this surface at scale. ## Divergence from the plan — two, both self-documented in source 1. **Eviction is minimum-weight, not LRU.** The ROADMAP (and the `0.1.0` CHANGELOG entry) say "LRU eviction". The shipped policy is a weight-based batch eviction: when the edge count exceeds capacity, edges are removed in ascending weight order until the count is back at capacity. The module header states the reasoning explicitly and says it "is NOT a true LRU policy" — strongest co-occurrence edges carry the most recommendation value (Sarwar et al., 2001), so signal strength beats recency here. The behaviour is deliberate and better justified than the plan; only the plan's wording is wrong. 2. **The `related` blend is additive, not a convex combination.** The ROADMAP criterion reads `final = embedding_sim × 0.6 + co_engagement × 0.3 + signal_score × 0.1`. The effective formula is `base_signal_score + boost_sum + co_eng_score × 0.3` (`tidal/src/ranking/executor/mod.rs:775-796`). The comment there explains why: embedding similarity is not available as a per-candidate scoring signal because ANN retrieval uses the embedding for candidate *selection*, not re-scoring. Folding `embedding_sim` in multiplicatively needs per-candidate ANN distances plumbed through the executor context. That plumbing arrived with M12 (`for_you` / `related` ANN candidate generation, see [milestone-12/phase-2.md](../milestone-12/phase-2.md)), so the blend is now revisitable — it has not been revisited.