Commit Graph

35 Commits

Author SHA1 Message Date
Cal Corum
7f17c9b9f2 fix: address PR #177 review — move import os to top-level, add audit idempotency guard
- Move `import os` from inside evaluate_game() to module top-level imports
  (lazy imports are only for circular dependency avoidance)
- Add get_or_none idempotency guard before RefractorBoostAudit.create()
  inside db.atomic() to prevent IntegrityError on UNIQUE(card_state, tier)
  constraint in PostgreSQL when apply_tier_boost is called twice for the
  same tier
- Update atomicity test stub to provide card_state/tier attributes for
  the new Peewee expression in the idempotency guard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:16:27 -05:00
Cal Corum
6a176af7da feat: Refractor Phase 2 integration — wire boost into evaluate-game
When a card reaches a new Refractor tier during game evaluation, the
system now creates a boosted variant card with modified ratings. This
connects the Phase 2 Foundation pure functions (PR #176) to the live
evaluate-game endpoint.

Key changes:
- evaluate_card() gains dry_run parameter so apply_tier_boost() is the
  sole writer of current_tier, ensuring atomicity with variant creation
- apply_tier_boost() orchestrates the full boost flow: source card
  lookup, boost application, variant card + ratings creation, audit
  record, and atomic state mutations inside db.atomic()
- evaluate_game() calls evaluate_card(dry_run=True) then loops through
  intermediate tiers on tier-up, with error isolation per player
- Display stat helpers compute fresh avg/obp/slg for variant cards
- REFRACTOR_BOOST_ENABLED env var provides a kill switch
- 51 new tests: unit tests for display stats, integration tests for
  orchestration, HTTP endpoint tests for multi-tier jumps, pitcher
  path, kill switch, atomicity, idempotency, and cross-player isolation
- Clarified all "79-sum" references to note the 108-total card invariant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 13:04:52 -05:00
Cal Corum
776f1a5302 fix: address PR review findings — rename evolution_tier to refractor_tier
- Rename `evolution_tier` parameter to `refractor_tier` in compute_variant_hash()
  to match the refractor naming convention established in PR #131
- Update hash input dict key accordingly (safe: function is new, no stored hashes)
- Update test docstrings referencing the old parameter name
- Remove redundant parentheses on boost_delta_json TextField declaration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:06:38 -05:00
Cal Corum
4a1251a734 feat: add Refractor Phase 2 foundation — boost functions, schema, tests
Pure functions for computing boosted card ratings when a player
reaches a new Refractor tier. Batter boost applies fixed +0.5 to
four offensive columns per tier; pitcher boost uses a 1.5 TB-budget
priority algorithm. Both preserve the 108-sum invariant.

- Create refractor_boost.py with apply_batter_boost, apply_pitcher_boost,
  and compute_variant_hash (Decimal arithmetic, zero-floor truncation)
- Add RefractorBoostAudit model, Card.variant, BattingCard/PitchingCard
  image_url, RefractorCardState.variant fields to db_engine.py
- Add migration SQL for refractor_card_state.variant column and
  refractor_boost_audit table (JSONB, UNIQUE constraint, transactional)
- 26 unit tests covering 108-sum invariant, deltas, truncation, TB
  accounting, determinism, x-check protection, and variant hash behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:39:03 -05:00
Cal Corum
537eabcc4d feat: add evaluated_only filter to GET /api/v2/refractor/cards (#174)
Closes #174

Adds `evaluated_only: bool = Query(default=True)` to `list_card_states()`.
When True (the default), cards with `last_evaluated_at IS NULL` are excluded —
these are placeholder rows created at pack-open time but never run through the
evaluator. At team scale this eliminates ~2739 zero-value rows from the
default response, making the Discord /refractor status command efficient
without any bot-side changes.

Set `evaluated_only=false` to include all rows (admin/pipeline use case).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 17:32:59 -05:00
Cal Corum
906d6e575a test: add Tier 3 refractor test cases (T3-1, T3-6, T3-7, T3-8)
Adds four Tier 3 (medium-priority) test cases to the existing refractor test
suite.  All tests use SQLite in-memory databases and run without a PostgreSQL
connection.

T3-1 (test_refractor_track_api.py): Two tests verifying that
  GET /api/v2/refractor/tracks?card_type= returns 200 with count=0 for both
  an unrecognised card_type value ('foo') and an empty string, rather than
  a 4xx/5xx.  A full SQLite-backed TestClient is added to the track API test
  module for these cases.

T3-6 (test_refractor_state_api.py): Verifies that
  GET /api/v2/refractor/cards/{card_id} returns last_evaluated_at: null (not
  a crash or missing key) when the RefractorCardState was initialised but
  never evaluated.  Adds the SQLite test infrastructure (models, fixtures,
  helper factories, TestClient) to the state API test module.

T3-7 (test_refractor_evaluator.py): Two tests covering fully_evolved/tier
  mismatch correction.  When the database has fully_evolved=True but
  current_tier=3 (corruption), evaluate_card must re-derive fully_evolved
  from the freshly-computed tier (False for tier 3, True for tier 4).

T3-8 (test_refractor_evaluator.py): Two tests confirming per-team stat
  isolation.  A player with BattingSeasonStats on two different teams must
  have each team's RefractorCardState reflect only that team's stats — not
  a combined total.  Covers both same-season and multi-season scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:38:25 -05:00
Cal Corum
569dc53c00 test: add Tier 1 and Tier 2 refractor system test cases
Implements all gap tests identified in the PO review for the refractor
card progression system (Phase 1 foundation).

TIER 1 (critical):
- T1-1: Negative singles guard in compute_batter_value — documents that
  hits=1, doubles=1, triples=1 produces singles=-1 and flows through
  unclamped (value=8.0, not 10.0)
- T1-2: SP tier boundary precision with floats — outs=29 (IP=9.666) stays
  T0, outs=30 (IP=10.0) promotes to T1; also covers T2 float boundary
- T1-3: evaluate-game with non-existent game_id returns 200 with empty results
- T1-4: Seed threshold ordering + positivity invariant (t1<t2<t3<t4, all >0)

TIER 2 (high):
- T2-1: fully_evolved=True persists when stats are zeroed or drop below
  previous tier — no-regression applies to both tier and fully_evolved flag
- T2-2: Parametrized edge cases for _determine_card_type: DH, C, 2B, empty
  string, None, and compound "SP/RP" (resolves to "sp", SP checked first)
- T2-3: evaluate-game with zero StratPlay rows returns empty batch result
- T2-4: GET /teams/{id}/refractors with valid team and zero states is empty
- T2-5: GET /teams/99999/refractors documents 200+empty (no team existence check)
- T2-6: POST /cards/{id}/evaluate with zero season stats stays at T0 value=0.0
- T2-9: Per-player error isolation — patches source module so router's local
  from-import picks up the patched version; one failure, one success = evaluated=1
- T2-10: Each card_type has exactly one RefractorTrack after seeding

All 101 tests pass (15 PostgreSQL-only tests skip without POSTGRES_HOST).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 09:02:30 -05:00
Cal Corum
500a8f3848 fix: complete remaining evolution→refractor renames from review
- Rename route /{team_id}/evolutions → /{team_id}/refractors
- Rename function initialize_card_evolution → initialize_card_refractor
- Rename index names in migration SQL
- Update all test references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:17:03 -05:00
Cal Corum
b7dec3f231 refactor: rename evolution system to refractor
Complete rename of the card progression system from "Evolution" to
"Refractor" across all code, routes, models, services, seeds, and tests.

- Route prefix: /api/v2/evolution → /api/v2/refractor
- Model classes: EvolutionTrack → RefractorTrack, etc.
- 12 files renamed, 8 files content-edited
- New migration to rename DB tables
- 117 tests pass, no logic changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:31:55 -05:00
Cal Corum
46c85e6874 fix: stale docstring + add decision-only pitcher test
- evaluate_card() docstring: "Override for PlayerSeasonStats" →
  "Override for BattingSeasonStats/PitchingSeasonStats"
- New test_decision_only_pitcher: exercises the edge case where a pitcher
  has a Decision row but no StratPlay rows, verifying _get_player_pairs()
  correctly includes them via the Decision table scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:31:16 -05:00
Cal Corum
1b4eab9d99 refactor: replace incremental delta upserts with full recalculation in season stats
The previous approach accumulated per-game deltas into season stats rows,
which was fragile — partial processing corrupted stats, upsert bugs
compounded, and there was no self-healing mechanism.

Now update_season_stats() recomputes full season totals from all StratPlay
rows for each affected player whenever a game is processed. The result
replaces whatever was stored, eliminating double-counting and enabling
self-healing via force=True.

Also fixes:
- evolution_evaluator.py: broken PlayerSeasonStats import → queries
  BattingSeasonStats or PitchingSeasonStats based on card_type
- evolution_evaluator.py: r.k → r.strikeouts
- test_evolution_models.py, test_postgame_evolution.py: PlayerSeasonStats
  → BattingSeasonStats (model never existed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:17:13 -05:00
Cal Corum
a2d2aa3d31 feat(WP-13): post-game callback endpoints for season stats and evolution
Implements two new API endpoints the bot calls after a game completes:

  POST /api/v2/season-stats/update-game/{game_id}
    Delegates to update_season_stats() service (WP-05). Returns
    {"updated": N, "skipped": bool} with idempotency via ProcessedGame ledger.

  POST /api/v2/evolution/evaluate-game/{game_id}
    Finds all (player_id, team_id) pairs from the game's StratPlay rows,
    calls evaluate_card() for each pair that has an EvolutionCardState,
    and returns {"evaluated": N, "tier_ups": [...]} with full tier-up detail.

New files:
  app/services/evolution_evaluator.py — evaluate_card() service (WP-08)
  tests/test_postgame_evolution.py    — 10 integration tests (all pass)

Modified files:
  app/routers_v2/season_stats.py — rewritten to delegate to the service
  app/routers_v2/evolution.py    — evaluate-game endpoint added
  app/main.py                    — season_stats router registered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 16:05:21 -05:00
Cal Corum
503e570da5 fix: add missing pg_conn fixture to conftest.py
Session-scoped psycopg2 fixture that skips gracefully when
POSTGRES_HOST is absent (local dev) and connects in CI. Required
by seeded_data/seeded_tracks fixtures in evolution API tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:33:02 -05:00
Cal Corum
583bde73a9 feat(WP-07): card state API endpoints — closes #72
Add two endpoints for reading EvolutionCardState:

  GET /api/v2/teams/{team_id}/evolutions
    - Optional filters: card_type, tier
    - Pagination: page / per_page (default 10, max 100)
    - Joins EvolutionTrack so card_type filter is a single query
    - Returns {count, items} with full card state + threshold context

  GET /api/v2/evolution/cards/{card_id}
    - Resolves card_id -> (player_id, team_id) via Card table
    - Duplicate cards for same player+team share one state row
    - Returns 404 when card missing or has no evolution state

Both endpoints:
  - Require bearer token auth (valid_token dependency)
  - Embed the EvolutionTrack in each item (not just the FK id)
  - Compute next_threshold: threshold for tier above current (null at T4)
  - Share _build_card_state_response() helper in evolution.py

Also cleans up 30 pre-existing ruff violations in teams.py that were
blocking the pre-commit hook: F541 bare f-strings, E712 boolean
comparisons (now noqa where Peewee ORM requires == False/True),
and F841 unused variable assignments.

Tests: tests/test_evolution_state_api.py — 10 integration tests that
skip automatically without POSTGRES_HOST, following the same pattern as
test_evolution_track_api.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:33:02 -05:00
cal
f5b24cf8f2 Merge pull request 'feat(WP-10): pack opening hook — evolution_card_state initialization' (#107) from feature/wp10-pack-opening-hook into card-evolution
Merge PR #107: WP-10 pack opening hook — evolution_card_state initialization
2026-03-18 20:31:59 +00:00
Cal Corum
64b6225c41 fix: align naming between evaluator, formula engine, and DB models
- Rename _CareerTotals.k → .strikeouts to match formula engine's
  stats.strikeouts Protocol
- Update test stubs: TrackStub fields t1→t1_threshold etc. to match
  EvolutionTrack model
- Fix fully_evolved logic: derive from post-max current_tier, not
  new_tier (prevents contradictory state on tier regression)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:31:44 -05:00
Cal Corum
fe3dc0e4d2 feat: WP-08 evaluate endpoint and evolution evaluator service (#73)
Closes #73

Adds POST /api/v2/evolution/cards/{card_id}/evaluate — force-recalculates
a card's evolution state from career totals (SUM across all
player_season_stats rows for the player-team pair).

Changes:
- app/services/evolution_evaluator.py: evaluate_card() function that
  aggregates career stats, delegates to formula engine for value/tier
  computation, updates evolution_card_state with no-regression guarantee
- app/routers_v2/evolution.py: POST /cards/{card_id}/evaluate endpoint
  plus existing GET /tracks and GET /tracks/{id} endpoints (WP-06)
- tests/test_evolution_evaluator.py: 15 unit tests covering tier
  assignment, advancement, partial progress, idempotency, fully evolved,
  no regression, multi-season aggregation, missing state error, and
  return shape
- tests/__init__.py, tests/conftest.py: shared test infrastructure

All 15 tests pass. Models and formula engine are lazily imported so
this module is safely importable before WP-01/WP-05/WP-07/WP-09 merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:31:44 -05:00
cal
c69082e3ee Merge pull request 'feat: add ProcessedGame ledger for full idempotency in update_season_stats() (#105)' (#106) from ai/paper-dynasty-database#105 into card-evolution
Merge PR #106: ProcessedGame ledger for full idempotency in update_season_stats()
2026-03-18 20:30:35 +00:00
Cal Corum
eba23369ca fix: align tier_from_value with DB model field names (t1_threshold)
formula_engine.tier_from_value read track.t1/t2/t3/t4 but the
EvolutionTrack model defines t1_threshold/t2_threshold/etc. Updated
both the function and test fixtures to use the _threshold suffix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:07:07 -05:00
Cal Corum
264c7dc73c feat(WP-10): pack opening hook — evolution_card_state initialization
Closes #75.

New file app/services/evolution_init.py:
- _determine_card_type(player): pure fn mapping pos_1 to 'batter'/'sp'/'rp'
- initialize_card_evolution(player_id, team_id, card_type): get_or_create
  EvolutionCardState with current_tier=0, current_value=0.0, fully_evolved=False
- Safe failure: all exceptions caught and logged, never raises
- Idempotent: duplicate calls for same (player_id, team_id) are no-ops
  and do NOT reset existing evolution progress

Modified app/routers_v2/cards.py:
- Add WP-10 hook after Card.bulk_create in the POST endpoint
- For each card posted, call _determine_card_type + initialize_card_evolution
- Wrapped in try/except so evolution failures cannot block pack opening
- Fix pre-existing lint violations (unused lc_id, bare f-string, unused e)

New file tests/test_evolution_init.py (16 tests, all passing):
- Unit: track assignment for batter / SP / RP / CP positions
- Integration: first card creates state with zeroed fields
- Integration: duplicate card is a no-op (progress not reset)
- Integration: different players on same team get separate states
- Integration: card_type routes to correct EvolutionTrack
- Integration: missing track returns None gracefully

Fix tests/test_evolution_models.py: correct PlayerSeasonStats import/usage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 13:41:05 -05:00
Cal Corum
c935c50a96 feat: add ProcessedGame ledger for full idempotency in update_season_stats() (#105)
Closes #105

Replace the last_game FK guard in update_season_stats() with an atomic
INSERT into a new processed_game ledger table. The old guard only blocked
same-game immediate replay; it was silently bypassed if game G+1 was
processed first (last_game already overwritten). The ledger is keyed on
game_id so any re-delivery — including out-of-order — is caught reliably.

Changes:
- app/db_engine.py: add ProcessedGame model (game FK PK + processed_at)
- app/services/season_stats.py: replace last_game check with
  ProcessedGame.get_or_create(); import ProcessedGame; update docstrings
- migrations/2026-03-18_add_processed_game.sql: CREATE TABLE IF NOT EXISTS
  processed_game with FK to stratgame ON DELETE CASCADE
- tests/conftest.py: add ProcessedGame to imports and _TEST_MODELS list
- tests/test_season_stats_update.py: add test_out_of_order_replay_prevented;
  update test_double_count_prevention docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 01:05:31 -05:00
Cal Corum
b8c55b5723 fix: address PR #104 review feedback
- Correct idempotency guard docstring in update_season_stats() to
  accurately describe the last_game FK check limitation: only detects
  replay of the most-recently-processed game; out-of-order re-delivery
  (game G after G+1) bypasses the guard. References issue #105 for the
  planned ProcessedGame ledger fix.
- Fix migration card_type comment: 'batting' or 'pitching' → 'batter',
  'sp', or 'rp' to match actual seeded values.
- Remove local rarity fixture in test_season_stats_update.py that
  shadowed the conftest.py fixture; remove unused rarity parameter from
  player_batter and player_pitcher fixtures.
- Update test_double_count_prevention docstring to note the known
  out-of-order re-delivery limitation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 00:04:04 -05:00
Cal Corum
f7bc248a9f fix: address PR review findings
- CRITICAL: Fix migration FK refs player(id) → player(player_id)
- Remove dead is_start flag from pitching groups (no starts column)
- Fix hr → homerun in test make_play helper
- Add explanatory comment to ruff.toml
- Replace print() with logging in seed script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:38:12 -05:00
Cal Corum
da9eaa1692 test: add Phase 1a test suite (25 tests)
- test_evolution_models: 12 tests for EvolutionTrack, EvolutionCardState,
  EvolutionTierBoost, EvolutionCosmetic, and PlayerSeasonStats models
- test_evolution_seed: 7 tests for seed idempotency, thresholds, formulas
- test_season_stats_update: 6 tests for batting/pitching aggregation,
  Decision integration, double-count prevention, multi-game accumulation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 19:33:49 -05:00
Cal Corum
6580c1b431 refactor: deduplicate pitcher formula and test constants
All checks were successful
Build Docker Image / build (push) Successful in 8m46s
Extract shared pitcher value computation into _pitcher_value() helper.
Consolidate duplicated column lists and index helper in season stats tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:49:33 -05:00
Cal Corum
bd8e4578cc refactor: split PlayerSeasonStats into BattingSeasonStats and PitchingSeasonStats
Some checks failed
Build Docker Image / build (push) Has been cancelled
Separate batting and pitching into distinct tables with descriptive column
names. Eliminates naming collisions (so/k ambiguity) and column mismatches
between the ORM model and raw SQL. Each table now covers all aggregatable
fields from its source (BattingStat/PitchingStat) including sac, ibb, gidp,
earned_runs, runs_allowed, wild_pitches, balks, and games_started.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:43:22 -05:00
Cal Corum
4ed62dea2c refactor: rename PlayerSeasonStats so to so_batter and k to so_pitcher
All checks were successful
Build Docker Image / build (push) Successful in 8m41s
The single-letter `k` field was ambiguous and too short for comfortable use.
Rename to `so_pitcher` for clarity, and `so` to `so_batter` to distinguish
batting strikeouts from pitching strikeouts in the same model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:31:52 -05:00
cal
32ca21558e Merge pull request 'feat: Track Catalog API endpoints (WP-06) (#71)' (#86) from ai/paper-dynasty-database#71 into next-release
Some checks failed
Build Docker Image / build (push) Failing after 4m43s
Reviewed-on: #86
2026-03-16 16:14:58 +00:00
cal
01c8aa140c Merge pull request 'feat: PlayerSeasonStats Peewee model (#67)' (#82) from ai/paper-dynasty-database#67 into next-release
Some checks are pending
Build Docker Image / build (push) Waiting to run
Reviewed-on: #82
2026-03-16 16:13:06 +00:00
cal
223743d89f Merge pull request 'feat: evolution track seed data and tests (WP-03) (#68)' (#83) from ai/paper-dynasty-database#68 into next-release
Some checks are pending
Build Docker Image / build (push) Waiting to run
Reviewed-on: #83
2026-03-16 16:12:18 +00:00
Cal Corum
ddf6ff5961 feat: Track Catalog API endpoints (WP-06) (#71)
Closes #71

Adds GET /api/v2/evolution/tracks and GET /api/v2/evolution/tracks/{track_id}
endpoints for browsing evolution tracks and their thresholds.  Both endpoints
require Bearer token auth and return a track dict with formula and t1-t4
threshold fields.  The card_type query param filters the list endpoint.

EvolutionTrack is lazy-imported inside each handler so the app can start
before WP-01 (EvolutionTrack model) is merged into next-release.

Also suppresses pre-existing E402/F541 ruff warnings in app/main.py via
pyproject.toml per-file-ignores so the pre-commit hook does not block
unrelated future commits to that file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 20:40:38 -05:00
Cal Corum
40e988ac9d feat: formula engine for evolution value computation (WP-09)
Closes #74

Adds app/services/formula_engine.py with three pure formula functions
(compute_batter_value, compute_sp_value, compute_rp_value), a dispatch
helper (compute_value_for_track), and a tier classifier (tier_from_value).
Tier boundaries and thresholds match the locked seed data from WP-03.

Note: pitcher formulas use stats.k (not stats.so) to match the
PlayerSeasonStats model field name introduced in WP-02.

19 unit tests in tests/test_formula_engine.py — all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 19:34:40 -05:00
Cal Corum
25f04892c2 feat: evolution track seed data and tests (WP-03) (#68)
Closes #68

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 17:35:12 -05:00
Cal Corum
8dfc5ef371 fix: remove evolution models from WP-02 PR (#82)
Evolution models (EvolutionTrack, EvolutionCardState, EvolutionTierBoost,
EvolutionCosmetic), their re-export module, and tests were included in
this PR without disclosure. Removed to keep this PR scoped to
PlayerSeasonStats (WP-02) only per review feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 17:02:00 -05:00
Cal Corum
4bfd878486 feat: add PlayerSeasonStats Peewee model (#67)
Closes #67

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 16:35:02 -05:00