Fence off the play_events rows that mean 0.5 by construction - #80
Merged
Conversation
…ruction Phase 1. Three decisions wait on ADR-0004's data — its own completion-ratio threshold, ADR-0005's RADIO weights, and #53 — and the data does not start where everyone assumed. Until #57 the web client delivered a play the moment listening crossed `min(duration / 2, 4 min)` and sent `completion_ratio` as measured at that instant, never revising it. Verified again on the live database on 2026-08-02, by day and context: **795 of 823 rows predate the fix**, and every context before 2026-08-01 shows completions averaging 0.42–0.50 — including `library`, because the web derives context from the queue source and sends `library` for a library queue exactly as the native app does. From 2026-08-01 the same context reads 0.972 and 1.000, with skips at ≤0.1. So the clock restarted on 2026-08-01, not 2026-07-27: a month of trustworthy data lands around 2026-09-01. `FEEDBACK_TRUSTWORTHY_SINCE` and `trustworthy_feedback_only` make the boundary one call away instead of a matter of discipline. A date, not a client filter, because `play_events` records no client and `context` does not stand in for one — the good native rows cannot be separated from the bad web rows beside them, and selecting on the ratio would be circular. Excluded rather than deleted, for a reason the investigation turned up: the damage is not limited to the ratio. A track abandoned at 55% was recorded as a completion at ~0.5 rather than as a skip, so `outcome` is unreliable before the cutoff and skips are *under*-counted. What survives is that `skipped` and `rejected` rows describe real abandonments. `ambient._negative_signal` counts exactly those two over a rolling 90-day window — so **the live recommender was never poisoned**, it has been running on a slightly weak negative signal that heals around 2026-10-30. ADR-0005 also gains the `Implementation:` block it never had, which is why it looked unbuilt in a survey while `RADIO` had been shipping for weeks. Two follow-ups recorded: a `client` column, cheap now and worthless retroactively, which is the only thing that would have made this separable; and that whether the web still reports at all is unverified — every post-cutoff row carries the context the native app hardcodes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1. Three decisions wait on ADR-0004's data — its own completion-ratio threshold, ADR-0005's RADIO weights, and #53 — and the data doesn't start where we assumed.
What the live database says
Until #57, the web client delivered a play the moment listening crossed
min(duration / 2, 4 min)and sentcompletion_ratioas measured at that instant, never revising it. Verified again on 2026-08-02, by day and context:library)library, 2026-08-01 → 02795 of 823 rows predate the fix.
libraryis contaminated too because the web derivescontextfrom the queue source and sendslibraryfor a library queue — exactly as the native app does.So the clock restarted on 2026-08-01, not 2026-07-27. A month of trustworthy data lands around 2026-09-01, and all three decisions inherit that date.
Why a date, and why not delete
FEEDBACK_TRUSTWORTHY_SINCE/trustworthy_feedback_onlyput the boundary one call away instead of leaving it to discipline.A date, not a client filter:
play_eventsrecords no client andcontextdoesn't stand in for one, so the good native rows can't be separated from the bad web rows beside them. Selecting on the ratio would be circular — it's the variable being measured. The cutoff costs those native rows; nothing cheaper does.Excluded, not deleted, for a reason the investigation turned up: the damage isn't limited to the ratio. A track abandoned at 55% was recorded as a completion at ~0.5 rather than as a skip, so
outcomeis unreliable before the cutoff and skips are under-counted. What survives is thatskipped/rejectedrows describe real abandonments — an incomplete census, not a wrong one.The reassuring part
ambient._negative_signalcounts exactlyskippedandrejected, over a rolling 90-day window. The live recommender was never poisoned — it's been running on a slightly weak negative signal, which heals as the window passes the cutoff (~2026-10-30). Nothing readscompletion_ratioanywhere yet, so the threshold work is entirely ahead of us.Also
Implementation:block it never had — which is why it looked unbuilt in a survey whileRADIOhad been shipping for weeks.clientcolumn (cheap now, worthless retroactively — the one thing that would have made this separable), and that whether the web still reports at all is unverified, since every post-cutoff row carries the context the native app hardcodes. Settle that by playing one track in the web app, not by reasoning about rows.Verification
Seven unit tests on the boundary, including that it's inclusive and naive-UTC to match
TIMESTAMP WITHOUT TIME ZONE.ruffclean. The DB-backed suite can't run here — no local postgres on 5432 — so CI covers it; the new module is pure and needs none.🤖 Generated with Claude Code
https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW