Companion to: MVP-782–789 follow-up · original Algo Signal Upgrades gist
Date: 2026-06-15 (updated — MeSH floor implemented)
Method: audited each ticket's acceptance criteria against sh-backend develop. Tests + the 4a floor fix added this pass.
| Ticket | Scope | Status |
|---|---|---|
| MVP-790 | Swipe L→R reduces tag weights, symmetrical with R→L, regenerates feed | 🟢 Done — all 5 ACs met; tests added |
| MVP-791 | Balanced SGC mix — 20% MeSH / 80% recent+medium | 🟢 Done bar one item — ratio/balance/DS2/edge-cases + MeSH floor done & tested; only non-MeSH subtype prioritization (4b) remains, product-gated |
Every acceptance criterion is satisfied:
| AC | Status | Evidence |
|---|---|---|
| R→L swipe boosts tag weights (primary + MeSH family) → new feed | ✅ | more_like_this = +1.5 (engagement.py:60); MeSH-relative propagation in record_post_engagement (engagement.py:285-288) |
| L→R swipe reduces tag weights (primary + MeSH family) → new feed | ✅ | less_like_this = −1.5; same propagation path (negative delta × relative weight) |
| Both gestures symmetrical, both trigger feed refresh | ✅ | ±1.5 symmetric; both persist to user_tag_affinity, which the next PersonalizedFeedAdapter fetch reads. FE issues the visible refresh |
| DS2 logs both as distinct signals with affected tags listed | ✅ | swipe_signals (dashboard.py:1752-1792) — grouped per gesture, each with a tags list of {tag_name, delta} |
| New feed visibly reflects the updated weights | ✅ | affinity-driven; visible in DS2 feed preview + sgc_breakdown |
Tests added (tests/test_engagement.py): test_swipe_gestures_are_symmetric_and_bonus_eligible — locks equal-magnitude/opposite-sign and confirms both gestures are MeSH-bonus eligible (not view-exempt). The per-tag + MeSH propagation itself is DB-bound and verified manually.
Verdict: closeable.
The mix is solid and wired into the live feed (apply_sgc_mix at feeds_http.py:580, fed by a 4× candidate pool). After this pass only the non-MeSH subtype personalization (4b) remains, and it is product-gated.
| AC | Status | Evidence |
|---|---|---|
| ~20% MeSH / ~80% non-MeSH | ✅ | apply_sgc_mix (feed_adapters.py:259) |
| 80% non-MeSH is itself a very-recent + medium-recent mix | ✅ | ~40/40 split (very_recent_target = ceil(nonmesh/2)) |
| DS2 shows SGC breakdown with actual % | ✅ | sgc_breakdown (dashboard.py:1826-1834, feed_adapters.py:347) |
| Narrow tag, scarce MeSH → fill what exists, fill rest non-MeSH, never short | ✅ | scarcity fallback (fills shortfall from leftovers, non-MeSH first) |
| Very-recent scarce → pull from medium-recent, never short | ✅ | same fallback path |
| Strong MeSH affinity → 20% is a FLOOR, not a ceiling | ✅ (fixed this pass) | see below |
Candidates arrive score-sorted from the affinity-aware PersonalizedFeedAdapter, so MeSH a user likes already ranks high. The previous hard cap (mesh_pool[:ceil(limit*0.20)]) truncated those high-affinity users back to 20%. Replaced with a floor that keeps however many MeSH naturally land in the top limit:
natural_mesh = sum(
1 for p in candidates[:limit]
if post_source_map.get(int(p.id)) in MESH_POST_SOURCES
)
mesh_target = min(limit, max(1, math.ceil(limit * 0.20), natural_mesh))High-affinity users now keep their affinity-ranked MeSH (>20%); everyone else still gets the 20% floor. Covered by test_mesh_share_is_a_floor_for_high_affinity_users (top-6 MeSH preserved) and test_mesh_floor_applies_when_affinity_is_low (pins 20% when MeSH ranks below the page).
| AC | Gap |
|---|---|
| Strong Pop/PopSci affinity → 80% prioritized toward preferred SGC subtype | Non-MeSH is split only by recency, never by subtype (Pop vs PopSci) weighted by engagement. apply_sgc_mix has no preference input. Only implicit via upstream score ordering. |
Needs a product call: does score-ordering already satisfy "prioritized toward Pop/PopSci," or is explicit per-subtype weighting (a new apply_sgc_mix parameter) required?
tests/test_sgc_mix.py— pure-function coverage ofapply_sgc_mix/sgc_breakdown:- 20/80 ratio (floor governs when MeSH isn't front-loaded), balanced non-MeSH split, never-exceeds-limit, empty input
- both scarcity edge cases (scarce MeSH; scarce very-recent → medium-recent)
- exact DS2 breakdown percentages
- MeSH floor both ways: high-affinity (>20% preserved) and low-affinity (pinned at 20%)
tests/test_engagement.py—test_swipe_gestures_are_symmetric_and_bonus_eligible(MVP-790)
Note: pytest's mypy-status item is red on a pre-existing, unrelated error (app/utils/analytics.py:79: posthog has no attribute "identify") — reproducible on already-committed tests (e.g. tests/test_bookmarks.py::mypy-status). Not introduced here.
- Close MVP-790 — all ACs met, tests added.
- MVP-791 — mechanical mix + edge cases + MeSH floor are done and tested. Only the non-MeSH subtype prioritization (4b) remains; split it into a product-gated sub-task and close the rest.
72472bf5— test: SGC-mix (791) + swipe-symmetry (790) coverage3d358d24— feat: make 20% MeSH share a floor not a ceiling (791 AC 4a)
app/sherpahealthy/posts/feed_adapters.py—apply_sgc_mix,sgc_breakdownapp/sherpahealthy/posts/feeds_http.py:520-583— 4× candidate pool + mix wiringapp/admin/dashboard.py:1752-1834— DS2swipe_signals+sgc_breakdownapp/sherpahealthy/engagement.py— swipe deltas + MeSH propagationtests/test_sgc_mix.py,tests/test_engagement.py