feat(crossposting): persist ranker decision log for retro analysis#211
Merged
ohld merged 1 commit intoproductionfrom Apr 28, 2026
Merged
feat(crossposting): persist ranker decision log for retro analysis#211ohld merged 1 commit intoproductionfrom
ohld merged 1 commit intoproductionfrom
Conversation
Adds a new table crossposting_decision_log that records, for every ranker call, the top-5 candidates with full per-multiplier score breakdown (lr/impr/age/caption/sent/src_quality/invited_boost/final), the median source-signal at decision time, and the total candidate pool size. This unlocks forensic algo retros after the source-quality CTE inputs roll out of the 30-day mature window: - distribution of clamp activations (was source_quality_mult binding?) - invited_count contribution vs other multipliers - pool size correlated with channel reach Refactors get_next_meme_for_tgchannelru/en to return (picked_meme, decision_log) tuples; flow handlers log the decision inside a try/except that mirrors the post-send safety pattern (a log-miss is acceptable, a Prefect retry republishing the album is not). Cost: ~10 INSERTs/day, ~2KB JSON each, ~7MB/year.
Member
Author
|
STAFF ENGINEER REVIEW: APPROVED — PR #211 (feat/ranker-decision-log) reviewed against production. Structural pass (Claude /review): clean.
Adversarial pass (Codex): 2 P2 findings, no P1.
Lower-priority observations (Claude):
None of these block the merge. Decision-log is observability — degraded analytic signal, not broken product. Auto-merge queued; lint passed, test pending. |
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.
Summary
Adds
crossposting_decision_logtable that records, per ranker call, the top-5 candidates with full per-multiplier score breakdown. Closes the forensic gap left by Phase 2 (#210): once the source-quality CTE inputs roll out of the 30-day mature window, we still know why each meme was picked.What's logged per call
Why
Phase 2 ranker has 7 multiplier components. After 30+ days, the source-quality CTE inputs roll out and we can't retroactively reconstruct what the ranker saw. Without decision logging, the only retro we can do is "did v2 beat v1?" — not "which multiplier dominated?"
This unlocks queries like:
src_quality_multhit 0.5/2.0?Implementation
src/database.py: newcrossposting_decision_logtable (id, decided_at, channel, picked_meme_id FK, score_version, median_signal, candidate_pool_size, candidates JSONB) + composite index on(channel, decided_at).alembic/versions/2026-04-28_add_crossposting_decision_log_table.py: clean migration (single head verified).src/crossposting/service.py:get_next_meme_for_tgchannelru/enrefactored to return(picked_meme, decision_log)tuple. BothNoneif no candidates pass filters.meme_statsfields +src_signal+median_signal+COUNT(*) OVER ()for pool size — ORDER BY identical to before so picking semantics are unchanged.LIMIT :limit(default 5)._compute_score_breakdown(row, channel)mirrors the SQL ORDER BY in Python — the table at the top of the function maps per-channel constants (impr_penalty,age_threshold).log_ranker_decision()writes the JSONB row.src/flows/crossposting/meme.py: handlers unpack the tuple, calllog_ranker_decisioninside try/except (a log miss is acceptable; a Prefect retry republishing the album is not — same safety pattern as the recent feat(crossposting): source-quality ranker + diversity cap (Phase 2) #210 fix).Verification
docker compose exec app pytest tests/test_crossposting_meme.py):_clean_captionteststest_ranker_decision_log_records_top5— full schema check, top-5 captured, pool size correcttest_ranker_decision_log_does_not_propagate_db_errors— invalid args raise (caller wraps in try/except)Cost
~10 INSERTs/day (5 RU + 5 EN crons), ~2KB JSON each = 20KB/day = 7MB/year. One additional SQL roundtrip per crosspost (negligible).
Read patterns (post-deploy retro queries)
Test plan
Predecessor
#210 — Phase 2 source-quality ranker (merged 2026-04-28)