MVP-782–789 Follow-up Analysis & Ticket Status (code audit, 2026-06-15; updated 2026-06-22)
Follows up on: Algo Signal Upgrades gist
Date: 2026-06-15 (updated 2026-06-22 — MVP-789 product decision resolved)
Method: direct audit of sh-backend develop against each ticket's acceptance criteria (file:line references below)
| Ticket | Scope | Status | Notes |
|---|---|---|---|
| MVP-782 | Feed position mismatch (TestFlight vs DS2) | 🔴 Open | Proposed session_id fix not implemented in the debug endpoint |
| MVP-783 | user_engagement_events log + DS2 all-signals page |
🟢 Done | Table, migrations (supabase+alembic), model, DS2 query all present |
| MVP-784 | Swipe left/right as strong symmetric signal | 🟢 Done | ±1.5, wired with post_source |
| MVP-785 | Post-detail click as strong signal | 🟢 Done | detail_view = +1.0, wired with post_source |
| MVP-786 | Original-source click as very strong signal | 🟢 Done | POST /v1/next/post/{id}/source-click, source_click = +2.5 |
| MVP-787 | Follow/unfollow strong + MeSH inheritance | 🟢 Done (demo-verified) | +2.0 / −1.5, MeSH propagation; ⚠ no automated tests |
| MVP-788 | MeSH tag hierarchy inheritance (foundation) | 🟢 Done (demo-verified) | get_mesh_relatives() cache + propagation; ⚠ no automated tests |
| MVP-789 | Triple bonus for MeSH post interactions | 🟢 Done | Post-based signals wired + tested; follow/unfollow decision resolved → Option 1 (2026-06-20) |
The gist's fix was: "Add session_id param to GET /v1/posts/next/feed/debug, pass it to _execute_feed_query so FeedSession boosts are applied."
Reality: feed_debug() (app/sherpahealthy/posts/feeds_http.py:1167) accepts only limit and profile_id — there is no session_id parameter and nothing is threaded into the feed query. DS2 therefore still scores raw affinity without session boosts, which is the leading hypothesis for the 238/96/3/4/165 mismatch.
To close: add session_id to feed_debug and get_algo_debug_data, apply FeedSession boosted/suppressed tags, surface the resolved session_id in DS2 output for QA. Then re-run the TestFlight-vs-DS2 comparison. This is the only ticket with no implementation at all.
- Table + indexes:
supabase/migrations/20260426000000_add_user_engagement_events.sql,20260507101106_add_uee_dedup_index.sql,20260512000000_add_uee_post_level_dedup_index.sqlwith Alembic parity (4aeb533fcfda_…,7f360f3b822c_…). - Model in
app/db/models.py; written on every signal via_log_event(). - DS2 signals page now queries
user_engagement_eventsfor all event types (app/admin/dashboard.py:2090-2118), not just the olduser_signal_activityview.
_DELTA_MORE_LIKE_THIS = 1.5, _DELTA_LESS_LIKE_THIS = -1.5 (engagement.py:62-63), symmetric per Confluence. Wired through /feed/signal with post_source (feeds_http.py:1093), so swipes also get the MVP-789 bonus on MeSH posts.
detail_view = +1.0 event added; get_post_detail() records it with post.post_source (feeds_http.py:936). Open-question #1 (prefetch over-crediting) is a product/FE follow-up, not a code gap.
POST /v1/next/post/{post_id}/source-click (feeds_http.py:969) records source_click = +2.5 with post_source. New endpoint exists and is the strongest non-share signal as specced.
- Weights:
tag_follow = +2.0,tag_unfollow = −1.5(asymmetric, locked decision) —engagement.py:58-59. - MeSH propagation on follow/unfollow:
record_tag_engagementinengagement.py, wired to live endpoints (follows.py:165, 429). - ⚠ Caveat: no unit tests assert affinity changes on follow/unfollow. Closure currently rests on a manual demo (REPL
get_mesh_relatives()+ before/afteruser_tag_affinityrows). Recommend a regression test before considering it hardened.
app/sherpahealthy/mesh_signal.py:138get_mesh_relatives()over a process-singleton cache built frommesh_trees.treeiddot-notation prefixes; weights match spec exactly (parent .40 / child .30 / sibling .20 / grandparent .20 / aunt-uncle .15,mesh_signal.py:23-29), capped at 2 levels.- Propagation in
record_post_engagement()(engagement.py:285-288). - ⚠ Same caveat: no automated coverage of
mesh_signal.py. A REPL cold-vs-warm timing also answers the gist's open-question #5 (inline vs nightly).
The framework was correct (_effective_delta() engagement.py:116-121, MESH_POST_SOURCES = {"PubMed","MeSH Descriptor","MeSH Concept"} — which also resolves open-question #4). The original audit found post_source defaulted to None, so the bonus silently no-opped at any call site that didn't pass it.
Post-based signals — fixed in the earlier follow-up. post_source is now threaded into the four post-based sites — comments.py (comment), reactions.py (like), bookmarks.py add/remove (bookmark / remove_bookmark) — joining the already-wired detail_view, source_click, and swipes. tests/test_engagement.py (the _effective_delta matrix) asserts the full table, including the headline "bookmark on a PubMed post → +3.0", view-never-tripled, and the MESH_POST_SOURCES discriminator.
Follow/unfollow product decision — RESOLVED 2026-06-20 → Option 1. tag_follow / tag_unfollow go through record_tag_engagement, which is post-agnostic (a follow is on a tag, not a post), so there is no post_source to triple. Decision: exclude follow/unfollow from the MeSH bonus and keep their flat weights (+2.0 / −1.5); drop the spec's "tag_follow → +6.0 on MeSH post" rows from the AC. Option 2 (gate on tag.mesh_descriptor_id) was rejected — it would introduce a second, tag-column-based definition of "MeSH" that drifts from the post-side MESH_POST_SOURCES single source of truth, and is a leaky proxy: gating on mesh_descriptor_id alone excludes 1,409 / 20,812 tags (6.8%), ~1,296 of which are genuinely MeSH-derived (concept/qualifier-mapped) but lack a descriptor FK; only ~6 real tags carry no MeSH linkage at all. Note MESH_POST_SOURCES already includes "MeSH Concept", so a descriptor-only gate would be narrower than the post-side definition.
Shipped change (commit 294a8747): removed the unused post_source parameter from record_tag_engagement() so the flat SCORE_DELTAS weight is used directly — eliminating the footgun that a future caller could pass post_source and silently re-apply the bonus on follows. No behaviour change (no caller ever passed it); the decision is now enforced in code, not just documented. Spec updated (specs/20260426-MVP-782-789-algo-signal-upgrades.md, MVP-789 section + signal score table). MVP-789 is now fully closed.
787/788 have no automated tests (tests/ references neither get_mesh_relatives nor record_tag_engagement; test_follows.py makes no affinity assertions). They are closeable on a manual demo, but if CI coverage is a closure requirement, add tests/test_mesh_signal.py + a follow/unfollow affinity assertion first.
- Close 783, 784, 785, 786 — fully implemented, no caveats.
- Close 787, 788 — demo-verified; optionally gate on a small regression test.
- Close 789 — post-based scope wired + tested; follow/unfollow product decision resolved (Option 1) and enforced in code (
294a8747). No remaining sub-task. - 782 — keep open; it is the only ticket with zero implementation. Add
session_idto the debug endpoint and re-run the QA comparison.
- Original gist: https://gist.github.com/aldrinleal/40ac1538ca5e3226111269b26deacb87
app/sherpahealthy/engagement.py,app/sherpahealthy/mesh_signal.py,app/sherpahealthy/follows.pyapp/sherpahealthy/posts/feeds_http.py,app/admin/dashboard.pytests/test_engagement.py- Spec:
specs/20260426-MVP-782-789-algo-signal-upgrades.md