major-domo-v2/models/CLAUDE.md
Cal Corum ca325142d8 CLAUDE: Add comprehensive CLAUDE.md documentation files for AI agent guidance
Adding 17 CLAUDE.md files across the project to provide detailed context
and implementation guidelines for AI development agents:

Root Documentation:
- CLAUDE.md - Main project guide with Git workflow requirements

Component Documentation:
- commands/CLAUDE.md - Command architecture and patterns
- models/CLAUDE.md - Pydantic models and validation
- services/CLAUDE.md - Service layer and API interactions
- tasks/CLAUDE.md - Background tasks and automation
- tests/CLAUDE.md - Testing strategies and patterns
- utils/CLAUDE.md - Utility functions and decorators
- views/CLAUDE.md - Discord UI components and embeds

Command Package Documentation:
- commands/help/CLAUDE.md - Help system implementation
- commands/injuries/CLAUDE.md - Injury management commands
- commands/league/CLAUDE.md - League-wide commands
- commands/players/CLAUDE.md - Player information commands
- commands/profile/CLAUDE.md - User profile commands
- commands/teams/CLAUDE.md - Team information commands
- commands/transactions/CLAUDE.md - Transaction management
- commands/utilities/CLAUDE.md - Utility commands
- commands/voice/CLAUDE.md - Voice channel management

Key Updates:
- Updated .gitignore to track CLAUDE.md files in version control
- Added Git Workflow section requiring branch-based development
- Documented all architectural patterns and best practices
- Included comprehensive command/service implementation guides

These files provide essential context for AI agents working on the codebase,
ensuring consistent patterns, proper error handling, and maintainable code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 20:30:07 -05:00

22 KiB
Raw Permalink Blame History

Models Directory

The models directory contains Pydantic data models for Discord Bot v2.0, providing type-safe representations of all SBA (Strat-o-Matic Baseball Association) entities. All models inherit from SBABaseModel and follow consistent validation patterns.

Architecture

Pydantic Foundation

All models use Pydantic v2 with:

  • Automatic validation of field types and constraints
  • Serialization/deserialization for API interactions
  • Type safety with full IDE support
  • JSON schema generation for documentation
  • Field validation with custom validators

Base Model (base.py)

The foundation for all SBA models:

class SBABaseModel(BaseModel):
    model_config = {
        "validate_assignment": True,
        "use_enum_values": True,
        "arbitrary_types_allowed": True,
        "json_encoders": {datetime: lambda v: v.isoformat() if v else None}
    }

    id: Optional[int] = None
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

Breaking Changes (August 2025)

Database entities now require id fields since they're always fetched from the database:

  • Player model: id: int = Field(..., description="Player ID from database")
  • Team model: id: int = Field(..., description="Team ID from database")

Game Submission Models (January 2025)

New models for comprehensive game data submission from Google Sheets scorecards:

Play Model (play.py)

Represents a single play in a baseball game with complete statistics and game state.

Key Features:

  • 92 total fields supporting comprehensive play-by-play tracking
  • 68 fields from scorecard: All data read from Google Sheets Playtable
  • Required fields: game_id, play_num, pitcher_id, on_base_code, inning details, outs, scores
  • Base running: Tracks up to 3 runners with starting and ending positions
  • Statistics: PA, AB, H, HR, RBI, BB, SO, SB, CS, errors, and 20+ more
  • Advanced metrics: WPA, RE24, ballpark effects
  • Descriptive text generation: Automatic play descriptions for key plays display

Field Validators:

@field_validator('on_first_final')
@classmethod
def no_final_if_no_runner_one(cls, v, info):
    """Ensure on_first_final is None if no runner on first."""
    if info.data.get('on_first_id') is None:
        return None
    return v

Usage Example:

play = Play(
    id=1234,
    game_id=567,
    play_num=1,
    pitcher_id=100,
    batter_id=101,
    on_base_code="000",
    inning_half="top",
    inning_num=1,
    batting_order=1,
    starting_outs=0,
    away_score=0,
    home_score=0,
    homerun=1,
    rbi=1,
    wpa=0.15
)

# Generate human-readable description
description = play.descriptive_text(away_team, home_team)
# Output: "Top 1: (NYY) homers"

Field Categories:

  • Game Context: game_id, play_num, inning_half, inning_num, starting_outs
  • Players: batter_id, pitcher_id, catcher_id, defender_id, runner_id
  • Base Runners: on_first_id, on_second_id, on_third_id (with _final positions)
  • Offensive Stats: pa, ab, hit, rbi, double, triple, homerun, bb, so, hbp, sac
  • Defensive Stats: outs, error, wild_pitch, passed_ball, pick_off, balk
  • Advanced: wpa, re24_primary, re24_running, ballpark effects (bphr, bpfo, bp1b, bplo)
  • Pitching: pitcher_rest_outs, inherited_runners, inherited_scored, on_hook_for_loss

API-Populated Nested Objects:

The Play model includes optional nested object fields for all ID references. These are populated by the API endpoint to provide complete context without additional lookups:

class Play(SBABaseModel):
    # ID field with corresponding optional object
    game_id: int = Field(..., description="Game ID this play belongs to")
    game: Optional[Game] = Field(None, description="Game object (API-populated)")

    pitcher_id: int = Field(..., description="Pitcher ID")
    pitcher: Optional[Player] = Field(None, description="Pitcher object (API-populated)")

    batter_id: Optional[int] = Field(None, description="Batter ID")
    batter: Optional[Player] = Field(None, description="Batter object (API-populated)")

    # ... and so on for all player/team IDs

Pattern Details:

  • Placement: Optional object field immediately follows its corresponding ID field
  • Naming: Object field uses singular form of ID field name (e.g., batter_idbatter)
  • API Population: Database endpoint includes nested objects in response
  • Future Enhancement: Validators could ensure consistency between ID and object fields

ID Fields with Nested Objects:

  • game_idgame: Optional[Game]
  • pitcher_idpitcher: Optional[Player]
  • batter_idbatter: Optional[Player]
  • batter_team_idbatter_team: Optional[Team]
  • pitcher_team_idpitcher_team: Optional[Team]
  • on_first_idon_first: Optional[Player]
  • on_second_idon_second: Optional[Player]
  • on_third_idon_third: Optional[Player]
  • catcher_idcatcher: Optional[Player]
  • catcher_team_idcatcher_team: Optional[Team]
  • defender_iddefender: Optional[Player]
  • defender_team_iddefender_team: Optional[Team]
  • runner_idrunner: Optional[Player]
  • runner_team_idrunner_team: Optional[Team]

Usage Example:

# API returns play with nested objects populated
play = await play_service.get_play(play_id=123)

# Access nested objects directly without additional lookups
if play.batter:
    print(f"Batter: {play.batter.name}")
if play.pitcher:
    print(f"Pitcher: {play.pitcher.name}")
if play.game:
    print(f"Game: {play.game.matchup_display}")

Decision Model (decision.py)

Tracks pitching decisions (wins, losses, saves, holds) for game results.

Key Features:

  • Pitching decisions: Win, Loss, Save, Hold, Blown Save flags
  • Game metadata: game_id, season, week, game_num
  • Pitcher workload: rest_ip, rest_required, inherited runners
  • Human-readable repr: Shows decision type (W/L/SV/HLD/BS)

Usage Example:

decision = Decision(
    id=456,
    game_id=567,
    season=12,
    week=5,
    game_num=2,
    pitcher_id=200,
    team_id=10,
    win=1,  # Winning pitcher
    is_start=True,
    rest_ip=7.0,
    rest_required=4
)

print(decision)
# Output: Decision(pitcher_id=200, game_id=567, type=W)

Field Categories:

  • Game Context: game_id, season, week, game_num
  • Pitcher: pitcher_id, team_id
  • Decisions: win, loss, hold, is_save, b_save (all 0 or 1)
  • Workload: is_start, irunners, irunners_scored, rest_ip, rest_required

Data Pipeline:

Google Sheets Scorecard
       ↓
SheetsService.read_playtable_data() → 68 fields per play
       ↓
PlayService.create_plays_batch() → Validate with Play model
       ↓
Database API /plays endpoint
       ↓
PlayService.get_top_plays_by_wpa() → Return Play objects
       ↓
Play.descriptive_text() → Human-readable descriptions

Model Categories

Core Entities

League Structure

  • team.py - Team information, abbreviations, divisions, and organizational affiliates
  • division.py - Division structure and organization
  • manager.py - Team managers and ownership
  • standings.py - Team standings and rankings

Player Data

  • player.py - Core player information and identifiers
  • sbaplayer.py - Extended SBA-specific player data
  • batting_stats.py - Batting statistics and performance metrics
  • pitching_stats.py - Pitching statistics and performance metrics
  • roster.py - Team roster assignments and positions

Game Operations

  • game.py - Individual game results and scheduling
  • play.py (NEW - January 2025) - Play-by-play data for game submissions
  • decision.py (NEW - January 2025) - Pitching decisions and game results
  • transaction.py - Player transactions (trades, waivers, etc.)

Draft System

  • draft_pick.py - Individual draft pick information
  • draft_data.py - Draft round and selection data
  • draft_list.py - Complete draft lists and results

Custom Features

  • custom_command.py - User-created Discord commands

Custom Command Model (October 2025)

Optional Creator ID Field

The CustomCommand model now has an optional creator_id field to support different API endpoints:

class CustomCommand(SBABaseModel):
    id: int = Field(..., description="Database ID")
    name: str = Field(..., description="Command name (unique)")
    content: str = Field(..., description="Command response content")
    creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
    creator: Optional[CustomCommandCreator] = Field(None, description="Creator details")

Why Optional:

  • The /custom_commands/by_name/{name}/execute API endpoint returns command data without creator_id
  • This endpoint only needs to return the command content for execution
  • Other endpoints (get, create, update, delete) include full creator information

Usage Pattern:

# Execute endpoint - creator_id may be None
command, content = await custom_commands_service.execute_command("BPFO")
# command.creator_id might be None, but command.content is always present

# Get endpoint - creator_id is always present
command = await custom_commands_service.get_command_by_name("BPFO")
# command.creator_id is populated, command.creator has full details

Important Note: This change maintains backward compatibility while fixing validation errors when executing custom commands. Permission checks (edit/delete) use endpoints that return complete creator information.

Trade System

  • trade.py - Multi-team trade structures and validation

Legacy Models

  • current.py - Legacy model definitions for backward compatibility

Model Validation Patterns

Required Fields

Models distinguish between required and optional fields:

class Player(SBABaseModel):
    id: int = Field(..., description="Player ID from database")  # Required
    name: str = Field(..., description="Player full name")        # Required
    team_id: Optional[int] = None                                  # Optional
    position: Optional[str] = None                                 # Optional

Field Constraints

Models use Pydantic validators for data integrity:

class BattingStats(SBABaseModel):
    at_bats: int = Field(ge=0, description="At bats (non-negative)")
    hits: int = Field(ge=0, le=Field('at_bats'), description="Hits (cannot exceed at_bats)")

    @field_validator('batting_average')
    @classmethod
    def validate_batting_average(cls, v):
        if v is not None and not 0.0 <= v <= 1.0:
            raise ValueError('Batting average must be between 0.0 and 1.0')
        return v

Custom Validators

Models implement business logic validation:

class Transaction(SBABaseModel):
    transaction_type: str
    player_id: int
    from_team_id: Optional[int] = None
    to_team_id: Optional[int] = None

    @model_validator(mode='after')
    def validate_team_requirements(self):
        if self.transaction_type == 'trade':
            if not self.from_team_id or not self.to_team_id:
                raise ValueError('Trade transactions require both from_team_id and to_team_id')
        return self

API Integration

Data Transformation

Models provide methods for API interaction:

class Player(SBABaseModel):
    @classmethod
    def from_api_data(cls, data: Dict[str, Any]):
        """Create model instance from API response data."""
        if not data:
            raise ValueError(f"Cannot create {cls.__name__} from empty data")
        return cls(**data)

    def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:
        """Convert model to dictionary for API requests."""
        return self.model_dump(exclude_none=exclude_none)

Serialization Examples

Models handle various data formats:

# From API JSON
player_data = {"id": 123, "name": "Player Name", "team_id": 5}
player = Player.from_api_data(player_data)

# To API JSON
api_payload = player.to_dict(exclude_none=True)

# JSON string serialization
json_string = player.model_dump_json()

# From JSON string
player_copy = Player.model_validate_json(json_string)

Testing Requirements

Model Validation Testing

All model tests must provide complete data:

def test_player_creation():
    # ✅ Correct - provides required ID field
    player = Player(
        id=123,
        name="Test Player",
        team_id=5,
        position="1B"
    )
    assert player.id == 123

def test_incomplete_data():
    # ❌ This will fail - missing required ID
    with pytest.raises(ValidationError):
        Player(name="Test Player")  # Missing required id field

Test Data Patterns

Use helper functions for consistent test data:

def create_test_player(**overrides) -> Player:
    """Create a test player with default values."""
    defaults = {
        "id": 123,
        "name": "Test Player",
        "team_id": 1,
        "position": "1B"
    }
    defaults.update(overrides)
    return Player(**defaults)

def test_player_with_stats():
    player = create_test_player(name="Star Player")
    assert player.name == "Star Player"
    assert player.id == 123  # Default from helper

Field Types and Constraints

Common Field Patterns

Identifiers

id: int = Field(..., description="Database primary key")
player_id: int = Field(..., description="Foreign key to player")
team_id: Optional[int] = Field(None, description="Foreign key to team")

Names and Text

name: str = Field(..., min_length=1, max_length=100)
abbreviation: str = Field(..., min_length=2, max_length=5)
description: Optional[str] = Field(None, max_length=500)

Statistics

games_played: int = Field(ge=0, description="Games played (non-negative)")
batting_average: Optional[float] = Field(None, ge=0.0, le=1.0)
era: Optional[float] = Field(None, ge=0.0, description="Earned run average")

Dates and Times

game_date: Optional[datetime] = None
created_at: Optional[datetime] = None
season_year: int = Field(..., ge=1900, le=2100)

Model Relationships

Foreign Key Patterns

Models reference related entities via ID fields:

class Player(SBABaseModel):
    id: int
    team_id: Optional[int] = None  # References Team.id

class BattingStats(SBABaseModel):
    player_id: int  # References Player.id
    season: int
    team_id: int    # References Team.id

Nested Objects

Some models contain nested structures:

class CustomCommand(SBABaseModel):
    name: str
    creator: Manager  # Nested Manager object
    response: str

class DraftPick(SBABaseModel):
    pick_number: int
    player: Optional[Player] = None  # Optional nested Player
    team: Team                       # Required nested Team

Validation Error Handling

Common Validation Errors

  • Missing required fields - Provide all required model fields
  • Type mismatches - Ensure field types match model definitions
  • Constraint violations - Check field validators and constraints
  • Invalid nested objects - Validate all nested model data

Error Examples

try:
    player = Player(name="Test")  # Missing required id
except ValidationError as e:
    print(e.errors())
    # [{'type': 'missing', 'loc': ('id',), 'msg': 'Field required'}]

try:
    stats = BattingStats(hits=5, at_bats=3)  # hits > at_bats
except ValidationError as e:
    print(e.errors())
    # Constraint violation error

Performance Considerations

Model Instantiation

  • Use model_validate() for external data
  • Use model_construct() for trusted internal data (faster)
  • Cache model instances when possible
  • Avoid repeated validation of the same data

Memory Usage

  • Models are relatively lightweight
  • Nested objects can increase memory footprint
  • Consider using __slots__ for high-volume models
  • Use exclude_none=True to reduce serialization size

Development Guidelines

Adding New Models

  1. Inherit from SBABaseModel for consistency
  2. Define required fields explicitly with proper types
  3. Add field descriptions for documentation
  4. Include validation rules for data integrity
  5. Provide from_api_data() class method if needed
  6. Write comprehensive tests covering edge cases

Team Model Enhancements (January 2025)

Organizational Affiliate Methods

The Team model now includes methods to work with organizational affiliates (Major League, Minor League, and Injured List teams):

class Team(SBABaseModel):
    async def major_league_affiliate(self) -> 'Team':
        """Get the major league team for this organization via API call."""

    async def minor_league_affiliate(self) -> 'Team':
        """Get the minor league team for this organization via API call."""

    async def injured_list_affiliate(self) -> 'Team':
        """Get the injured list team for this organization via API call."""

    def is_same_organization(self, other_team: 'Team') -> bool:
        """Check if this team and another team are from the same organization."""

Usage Examples

Organizational Relationships

# Get affiliate teams
por_team = await team_service.get_team_by_abbrev("POR", 12)
por_mil = await por_team.minor_league_affiliate()  # Returns "PORMIL" team
por_il = await por_team.injured_list_affiliate()   # Returns "PORIL" team

# Check organizational relationships
assert por_team.is_same_organization(por_mil)  # True
assert por_team.is_same_organization(por_il)   # True

# Different organizations
nyy_team = await team_service.get_team_by_abbrev("NYY", 12)
assert not por_team.is_same_organization(nyy_team)  # False

Roster Type Detection

# Determine roster type from team abbreviation
assert por_team.roster_type() == RosterType.MAJOR_LEAGUE  # "POR"
assert por_mil.roster_type() == RosterType.MINOR_LEAGUE   # "PORMIL"
assert por_il.roster_type() == RosterType.INJURED_LIST    # "PORIL"

# Handle edge cases
bhm_il = Team(abbrev="BHMIL")  # BHM + IL, not BH + MIL
assert bhm_il.roster_type() == RosterType.INJURED_LIST

Implementation Notes

  • API Integration: Affiliate methods make actual API calls to fetch team data
  • Error Handling: Methods raise ValueError if affiliate teams cannot be found
  • Edge Cases: Correctly handles teams like "BHMIL" (Birmingham IL)
  • Performance: Base abbreviation extraction is cached internally

Model Evolution

  • Backward compatibility - Add optional fields for new features
  • Migration patterns - Handle schema changes gracefully
  • Version management - Document breaking changes
  • API alignment - Keep models synchronized with API

Testing Strategy

  • Unit tests for individual model validation
  • Integration tests with service layer
  • Edge case testing for validation rules
  • Performance tests for large data sets

Trade Model Enhancements (January 2025)

Multi-Team Trade Support

The Trade model now supports complex multi-team player exchanges with proper organizational authority handling:

class Trade(SBABaseModel):
    def get_participant_by_organization(self, team: Team) -> Optional[TradeParticipant]:
        """Find participant by organization affiliation.

        Major League team owners control their entire organization (ML/MiL/IL),
        so if a ML team is participating, their MiL and IL teams are also valid.
        """

    @property
    def cross_team_moves(self) -> List[TradeMove]:
        """Get all moves that cross team boundaries (deduplicated)."""

Key Features

Organizational Authority Model

# ML team owners can trade from/to any affiliate
wv_team = Team(abbrev="WV")     # Major League
wv_mil = Team(abbrev="WVMIL")   # Minor League
wv_il = Team(abbrev="WVIL")     # Injured List

# If WV is participating in trade, WVMIL and WVIL moves are valid
trade.add_participant(wv_team)  # Add ML team
# Now can move players to/from WVMIL and WVIL

Deduplication Fix

# Before: Each move appeared twice (giving + receiving perspective)
cross_moves = trade.cross_team_moves  # Would show duplicates

# After: Clean single view of each player exchange
cross_moves = trade.cross_team_moves  # Shows each move once

Trade Move Descriptions

Enhanced move descriptions with clear team-to-team visualization:

# Team-to-team trade
"🔄 Mike Trout: WV (ML) → NY (ML)"

# Free agency signing
" Mike Trout: FA → WV (ML)"

# Release to free agency
" Mike Trout: WV (ML) → FA"

Usage Examples

Basic Trade Setup

# Create trade
trade = Trade(trade_id="abc123", participants=[], status=TradeStatus.DRAFT)

# Add participating teams
wv_participant = trade.add_participant(wv_team)
ny_participant = trade.add_participant(ny_team)

# Create player moves
move = TradeMove(
    player=player,
    from_team=wv_team,
    to_team=ny_team,
    source_team=wv_team,
    destination_team=ny_team
)

Organizational Flexibility

# Trade builder allows MiL/IL destinations when ML team participates
builder = TradeBuilder(user_id, wv_team)  # WV is participating
builder.add_team(ny_team)

# This now works - can send player to NYMIL
success, error = await builder.add_player_move(
    player=player,
    from_team=wv_team,
    to_team=ny_mil_team,  # Minor league affiliate
    from_roster=RosterType.MAJOR_LEAGUE,
    to_roster=RosterType.MINOR_LEAGUE
)
assert success  # ✅ Works due to organizational authority

Implementation Notes

  • Deduplication: cross_team_moves now uses only moves_giving to avoid showing same move twice
  • Organizational Lookup: Trade participants can be found by any team in the organization
  • Validation: Trade balance validation ensures moves are properly matched
  • UI Integration: Embeds show clean, deduplicated player exchange lists

Breaking Changes Fixed

  • Team Roster Type Detection: Updated logic to handle edge cases like "BHMIL" correctly
  • Autocomplete Functions: Fixed invalid parameter passing in team filtering
  • Trade Participant Validation: Now properly handles organizational affiliates

Next Steps for AI Agents:

  1. Review existing model implementations for patterns
  2. Understand the validation rules and field constraints
  3. Check the service layer integration in /services
  4. Follow the testing patterns with complete model data
  5. Consider the API data format when creating new models