claude/points-split-rounding-inconsistency-7yn7xp #141
Open
chrisp
wants to merge 4 commits from
claude/points-split-rounding-inconsistency-7yn7xp into main
pull from: claude/points-split-rounding-inconsistency-7yn7xp
merge into: chrisp:main
chrisp:main
chrisp:claude/gifted-allen-npeql8
chrisp:claude/admiring-bohr-2zvef9
chrisp:claude/session-crx3tm
chrisp:claude/mlb-simulator-projected-wins-elo-siess8
chrisp:claude/afl-ev-calc-issue-8kcmlh
chrisp:claude/llws-ev-calculation-bug-30xpxg
chrisp:claude/afl-top-4-guaranteed-points-lof6ze
chrisp:claude/llws-simulator-logic-0aqbjb
chrisp:claude/app-framework-research-afenwb
chrisp:claude/indycar-ev-sims-probability-fpzfx6
chrisp:claude/qp-points-bubble
chrisp:claude/qp-scoreboard-all-participants
chrisp:claude/qualifying-points-format-cs9qrv
chrisp:claude/wimbledon-qp-scoring-bug-xevhlf
chrisp:claude/settings-menu-urls-kbp6x8
chrisp:claude/trusting-brown-j575rt
chrisp:claude/cool-lamport-etpwsi
chrisp:claude/practical-newton-4v041g
chrisp:claude/charming-tesla-re9ur7
4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
75960a8826 |
Fix review findings in the tie-split ledger and backfill
A review of the previous two commits found five defects in the new ledger writer and backfill, plus one pre-existing scoring bug the refactor exposed. season_standings ties were never split. processSeasonStandings deliberately writes the same finalPosition to every driver in a tied group -- its comment says "the scoring system will handle averaging" -- but no path ever did, so two drivers tied for 3rd each banked the full 50 instead of the published 45. This predates the tie-split work; the original cascade had only bracket and qualifying_points arms. Introduce usesSharedPlacementSplit as the single definition of which patterns record ties as a repeated placement, and route both calculatePickPoints and every caller-side gate through it. The caller gates matter as much as the helper: a gate left hardcoded to qualifying_points silently passes a tie count of 1, which reads as "no tie" and makes the fix inert. The ledger anchor picked the wrong event. Ordering on completedAt with no isComplete filter ranked never-completed events first, because drizzle's desc() emits a bare desc and Postgres orders DESC as NULLS FIRST. Restrict to completed events and order explicitly with NULLS LAST plus a stable tiebreak. The anchor is also no longer load-bearing for idempotence: stale event-level rows for the sports season are cleared before writing, so a re-run whose anchor moved replaces rather than duplicates. A ledger failure could abort finalization. The call sat unguarded after the season was already marked completed, so a throw in any of its queries would skip the standings recalculation and the Discord notification. Guard both call sites the way the probability refresh directly below already is. Rows could be mislabelled permanently. The backfill passed no eventName, and the upsert never rewrote scoringEventName. Derive the label from the scoring pattern inside the writer so omitting it is impossible, and refresh it on conflict so existing rows can be repaired. The backfill damaged unrelated leagues. recalculateStandings rewrites previousRank, so sweeping every season wiped rank-movement arrows league wide, including leagues holding no tie at all. Scope it to seasons drafting from a sports season that actually contains a tied placement, and correct the docblock that called it a pure recompute. Also drops the inert Number.EPSILON guard from calculateAveragedPoints (EPSILON is below the ULP for any value >= 2, and integer averages landing on .5 are exactly representable) and extracts countSharedPlacements so the ledger writer stops re-querying rows it already holds. Every fix is covered by a test confirmed to fail when that fix alone is reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz |
||
|
|
c48d54d873 |
Add backfill for rounded tie splits and missing ledger rows
Two stored artifacts went stale when the tie-split math was unified. team_standings totals hold pre-rounding values, and standings are only rewritten when something re-triggers a recalculation, so already-finished leagues would keep 217.50-style figures indefinitely. Separately, qualifying_points and season_standings seasons finalized before this change have no team_score_events rows, so their results stay missing from Recent Scores. The script re-ledgers every already-finalized one-shot sports season and recalculates standings for every fantasy season. Both operations are pure recomputes and ledger rows upsert on (team, season, scoring event), so it is safe to re-run. Supports --dry, following backfill-qp-resplit.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz |
||
|
|
75798d9711 |
Record final-placement points in the score-events ledger
qualifying_points (golf, tennis, CS2) and season_standings (F1) award all of their points in one step at finalization, so they produce no per-match deltas. recordMatchScoreEvents is bracket-only and fires from match processing, which meant these sports were never written to team_score_events at all and silently never appeared in Recent Scores. Add recordFinalPlacementScoreEvents, called from finalizeQualifyingPoints and processSeasonStandings. It writes one row per team at the ledger's event-level grain, carrying that team's summed award and every contributing participant, with points from calculatePickPoints so a tied golfer contributes the same split award the standings show. The row is anchored to a real scoring event rather than a null one: the event-level unique index is (teamId, seasonId, scoringEventId) and Postgres treats NULLs as distinct, so a null anchor would duplicate rows on every re-finalization instead of upserting. When no anchor can be resolved the ledger write is skipped, which leaves standings unaffected. Adds regression coverage for the two screens that had diverged — getTeamScoreBreakdown and computeCoronaStates — including an assertion that the team page's actualPoints equals calculateTeamScore's totalPoints for the same roster, and cases proving undrafted participants still count toward a tie span. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz |
||
|
|
430526104c |
Unify tie-split point math across every screen
A participant tied for a scoring placement splits the combined points of the tied positions. Four code paths computed a pick's points, each re-implementing the same bracket/qualifying_points/default cascade, and two of them omitted the qualifying_points arm entirely. A golfer tied for 8th was therefore worth the full 15 points on the team page and draft board but the split 7.5 in the standings, so a team read 225 on one screen and 218 on another. Collapse the cascade into a single calculatePickPoints helper and route all six call sites through it, backed by one shared getSharedPlacementCounts loader replacing the two separate tie-count queries. Tie counts span every participant in the sports season, not just drafted ones, since an undrafted tie partner still halves the award. Also round split awards to the nearest whole point in calculateAveragedPoints. The /rules page states ties are "combined and split equally among them, rounded to the nearest whole point", and its own worked example rounds 18.33 down to 18, so this is nearest rather than ceiling. Season point values are integer columns, making this averaging the only source of fractional points; rounding here means the standings' 218 is now correct by construction rather than a display artifact, and per-pick values visibly sum to the team total. Existing assertions encoding the unrounded results are updated, and the rules page's two published examples are asserted directly so the code and the published rule cannot drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz |