Repository navigation
crossposting: one scoring module with named factors; LIVE picks per channel - #392
Merged
Merged
Conversation
…hannel) src/crossposting/scoring.py holds FACTORS, SCORES, PRESELECT, LIVE and SCORE_VERSION. SQL only fetches the eligible pool; Python preselects the top-5, attaches shadow inputs and picks with LIVE. Promoting a shadow score is now a one-line LIVE edit. Picks are unchanged. The old SQL ORDER BY had two quirks that the logged breakdown hid; both are kept on purpose: - :impr_penalty was bound into an integer CASE, so memes with raw_impr_rank > 1 always scored 0 (not 0.8 RU / 0.5 EN); - sources without enough history got src mult 0.5 (GREATEST ignores NULL), not the logged 1.0. Removes shadow_score.py, the duplicated breakdown, and the CROSSPOST_RU_MEME_LIKE_VOLUME_ENABLED setting (LIVE is the switch). Decision log keeps the legacy flat keys the readout SQL reads and adds factors/scores/ranks/picks/picked_by dicts.
… meme by id; readout and date nits
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.
What
All crosspost scores now live in one module,
src/crossposting/scoring.py:FACTORS: named factor functions(row, channel) -> float | None: the v2 parts,like_volume_factor,v5_base,content_mult_v1,src_fwd_mult.SCORES: maps each score name to the factors it multiplies:v2,v4,v5,v5c,v5f.PRESELECT/LIVE: which score picks the top-5 from the pool, and which score picks the meme from that top-5, per channel. Both are set to today's behaviour (RUv4, ENv2).SCORE_VERSION: maps score name to the int stored incrossposting.score_version/decision_log.score_version.To promote a shadow score, edit
LIVE(for exampleLIVE["tgchannelru"] = "v5f", which gives score_version 7). If the shadow inputs fail to load, the pick falls back toPRESELECT.SQL (
_POOL_QUERY) now only filters and fetches the eligible pool. Ordering and the pick happen in Python. Fetching the whole pool (~128k RU / 45k EN rows) took 1.3 s on prod. The share-click counts moved into the shadow-inputs query, so they are log-only and can no longer block a post.Deleted:
shadow_score.py,_compute_score_breakdown(the "must stay in sync" copy), both SQL ORDER BY formulas,attach_v5_ranks,shadow_version, and theCROSSPOST_RU_MEME_LIKE_VOLUME_ENABLEDsetting.LIVEis now the switch. The setting is env-only and pydantic ignores unknown env vars, so removing it is safe.Decision log
Each candidate now carries
factors,scores,ranks,picks(each score's would-be pick) andpicked_by. It also still emits the legacy flat keys that the readouts read (final_score,lr_factor,like_volume_*,score_v5/v5c/v5f,shadow_v5*_rank,shadow_v5*_pick_meme_id,content_mult_v1,src_fwd_pct,rank). Thedocs/analyst/crossposting-*.sqlfiles therefore work on old and new rows without changes. Caveat, unchanged from before: shadow scores are ranked only within the production top-5.Two existing quirks, kept so the picks don't change (product call needed)
The replay found that the old SQL ORDER BY did not match the logged breakdown:
CASE WHEN raw_impr_rank <= 1 THEN 1 ELSE :impr_penalty ENDgets typed as integer, so asyncpg sent 0 instead of 0.8 (RU) / 0.5 (EN). Any meme withraw_impr_rank > 1scored 0. Prod confirms it: across the last 40 decisions, every top-5 candidate hasraw_impr_rank0 or 1.GREATEST(0.5, NULL)= 0.5, so a source with fewer than 5 posts gotsrc_quality_mult0.5, not the neutral 1.0 that the spec intended and the log claimed.The code keeps both (
impr_penalty: 0.0,src_quality_multreturns 0.5) with comments.final_score/impr_factor/src_quality_multon new rows now show the values that actually ranked the memes. Fixing either quirk changes the picks, so it should be its own experiment.Proof of zero pick change
service.pyfromorigin/productionvs this branch, same DB). 120 random fixture sets (60 per channel) covering missing src quality, impression ranks 0/1/2/5/99999, captions, age thresholds,nmemes_sent0/1/n,invited_count> 10, videos (excluded), a source posted within 24h (excluded), NULL/present content tags and exact ties. Result: same pick, same top-5 order, same score_version and same pool size in 120/120.scoring.py: 40/40 picks and 40/40 top-5 orders match.scoring.pyover the full current pool (127,532 RU / 45,074 EN): same pick and same top-5 on both channels.Tests
tests/crossposting,tests/flows/crosspostingandtests/test_crossposting_meme.pypass (42).ruff checkandruff formatare clean.