Card Evolution: season stats full recalculation → next-release #112
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#112
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "card-evolution"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Promotes the season stats full-recalculation refactor (PR #111) from
card-evolutionintonext-release.Changes
update_season_stats()rewritten from incremental delta upserts to full recalculation from StratPlay rows — eliminates double-counting and enables self-healing viaforce=Trueevolution_evaluator.pyfixed — brokenPlayerSeasonStatsimport replaced with correctBattingSeasonStats/PitchingSeasonStatsqueries based on card_type;r.k→r.strikeoutsforcequery param passthrough onPOST /api/v2/season-stats/update-game/{game_id}PlayerSeasonStats→BattingSeasonStatsintest_evolution_models.pyandtest_postgame_evolution.pyTesting
Full test suite: 116 passed, 15 skipped (PG integration), 0 failed. Three rounds of PR review completed.
🤖 Generated with Claude Code
AI Code Review
Files Reviewed
app/routers_v2/season_stats.py(modified)app/services/season_stats.py(modified — full rewrite)app/services/evolution_evaluator.py(modified)tests/test_evolution_evaluator.py(modified)tests/test_evolution_models.py(modified)tests/test_postgame_evolution.py(modified)tests/test_season_stats_update.py(modified — 4 new tests)Findings
Correctness
Full recalculation logic is correct. The new
_recalc_battingand_recalc_pitchinghelpers aggregate fromStratPlayvia aJOINtoStratGameand filter byseason. This is semantically correct — thefull-seasontotals are computed from the source of truth rather than accumulated via deltas.gamescount for batters (_recalc_batting). Usesfn.COUNT(Case(None, [(StratPlay.pa > 0, StratPlay.game)], None).distinct()). TheCaseexpression returnsgame_idwhenpa > 0andNULLotherwise;COUNTignoresNULL;.distinct()ensures a player appearing multiple times in a game withpa > 0is counted once. This is correct and matches the prior logic (g["games"] = 1on firstpa > 0play).gamescount for pitchers (_recalc_pitching). Usesfn.COUNT(StratPlay.game.distinct())— counts all distinct game IDs regardless of play type. This matches the prior behavior where"games": 1was set unconditionally for all pitchers._recalc_decisionsusesDecision.seasondirectly. The Decision model has aseason = IntegerField()column (not a FK), so theWHERE Decision.season == seasonfilter is correct. This is the right table to query rather than joining through StratGame.evolution_evaluator.pyfix is correct. Ther.kattribute error (oldPlayerSeasonStatsmodel hadk, butBattingSeasonStats/PitchingSeasonStatsusestrikeouts) is resolved. The new branching logic oncard_typeproperly routes toBattingSeasonStatsorPitchingSeasonStatsand queries withplayer ==/team ==FK fields — Peewee resolves integer comparisons against FK field names correctly.force=Truepath is correct. Whennot created and force=True, the code logs and continues — it doesn't delete or update the ProcessedGame ledger row, which is the right behavior (the row still exists to track that the game was first processed)._stats_modeloverride path inevolution_evaluator.pyis correct. The test injection path (if _stats_model is not None) uses_stats_model.player_idand_stats_model.team_id— these are raw FK column accessors consistent with how the test stub model is defined (usingIntegerFielddirectly). The production path (when_stats_model is None) usesBattingSeasonStats.player/.teamFK fields — also correct.get_or_createinupdate_season_statsuses raw FK accessors (player_id=,team_id=). This is the established codebase pattern (confirmed in prior PR reviews #106, #107, #108) and is valid.obj.last_game_id = game_id— The model defineslast_game = ForeignKeyField(StratGame, null=True). Settinglast_game_id(the raw column) directly to an integer is valid in Peewee and avoids an unnecessary FK object fetch. Consistent with prior usage in the sqlite upsert paths being replaced.New docstring on
_CareerTotalscorrectly updated fromsp/rp: outs, ktosp/rp: outs, strikeouts.Decision-only pitcher test is correct.
test_decision_only_pitcherassertsstats.games == 0because the relief pitcher has no StratPlay rows, soCOUNT(DISTINCT game)from_recalc_pitchingreturns 0. This is an intentional, documented behavior change from the old code (which set"games": 1unconditionally in_apply_decisions). The comment in_get_player_pairsacknowledges this edge case.Security
No regressions. The
[REDACTED]log pattern is preserved inseason_stats.py(router). No user-controlled data flows into raw SQL. Theforcequery param is a boolean — no injection surface.Style & Conventions
The PR follows established codebase patterns throughout: lazy imports at call time (
from app.db_engine import ...inside theelseblock),get_or_createwith raw FK accessors,db.atomic()wrapping,logger.info/debugwith%-style formatting.One minor observation: The
_recalc_battingdocstring still says "TheCOUNT(DISTINCT ...)with aCASEexpression achieves this: NULL values from the CASE are ignored by COUNT" but the actual query uses Peewee's.distinct()chained onto theCase(...)expression, which Peewee renders asCOUNT(DISTINCT CASE ...). This is technically the same thing but the description could be clearer. Non-blocking.Suggestions
test_decision_only_pitcherassertsstats.games == 0: This is a documented intentional behavior, but it means a pitcher who received a win in a game they don't appear in via StratPlay rows will showgames=0, wins=1in their season stats — which is a valid edge case rather than a bug. Worth a comment in the test explaining this is intentional.No multi-season career accumulation test for
evolution_evaluator.py: The newelsebranch sums over all seasons. The existing test coverage intest_postgame_evolution.pyseeds a prior-season row (season=10) and a current game (season=11), which indirectly exercises this. Adequate coverage.Concurrent force=True calls: If two requests race with
force=Truefor the samegame_id, both bypass thenot createdguard and write simultaneously. Sinceget_or_create+ field assignment +save()is not atomic at the SQL level, a race could produce a partial write. This is the same risk documented in PR #106 and the full-recalculation approach actually makes it less dangerous (worst case: one overwrite wins, stats are still correct totals), so this is acceptable.Verdict: APPROVED
This PR correctly promotes the full-recalculation refactor from
card-evolutiontonext-release. The core rewrite eliminates the double-counting and out-of-order replay problems inherent to the incremental delta approach. All three helper functions (_recalc_batting,_recalc_pitching,_recalc_decisions) produce correct SQL aggregations. Theevolution_evaluator.pyfix resolves the brokenPlayerSeasonStatsimport andr.kattribute error. Theforce=Truepassthrough is clean. Test coverage is thorough (4 new tests covering force recalc, idempotency, self-healing, and decision-only pitcher).Automated review by Claude PR Reviewer