Add card builder architecture redesign documentation

Documents proposed architecture for moving card-building logic upstream
to Python, including:
- Executive summary with problem statement and migration path
- CardBuilder sketch with contract system for pluggable placement strategies
- Support for different card "personalities" (Standard, Clutch, Power Heavy, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-22 10:56:55 -06:00
parent a2f4d02b18
commit 9a121d370f
3 changed files with 2387 additions and 0 deletions

View File

@ -0,0 +1,209 @@
# Card Creation Architecture Redesign - Executive Summary
**Date:** 2026-01-22
**Project:** Paper Dynasty Card Generation System
**Author:** Cal Corum + Claude (Jarvis)
---
## Problem Statement
The current card creation pipeline has an architectural inconsistency:
1. **Python generates continuous chance values** (e.g., 4.78 out of 108)
2. **Database fits to discrete card mechanics** (2d6 × d20 combinations)
3. **Database saves fitted values back** - what you send ≠ what gets stored
This results in subtle discrepancies (e.g., sending 4.75, card shows 4.65) because the card mechanics only support specific discrete probability values.
### Root Cause
The card uses 2d6 (rows 2-12) combined with d20 splits. Valid chance values are constrained by:
- Row frequencies: 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1 (totaling 36)
- D20 subdivisions: 1-20 range creates fractional chances
Python generates values ignorant of these constraints; the database must "round" to fit.
---
## Proposed Solution
**Move the card-building logic upstream to Python**, making it the single source of truth.
### New Architecture
```
BEFORE:
Python: stats → continuous chances → POST raw values
Database: receives → fits to card → saves DIFFERENT values → renders
AFTER:
Python: stats → continuous chances → CardBuilder → discrete card structure → POST
Database: receives → stores → renders (no fitting needed)
```
### Key Components
| Component | Responsibility |
|-----------|----------------|
| `CardBuilder` | Fits continuous chances to discrete card structure |
| `CardContract` | Defines placement strategy (which rows for which plays) |
| `BuiltCard` | Structured output ready for storage/rendering |
| `RawBattingChances` / `RawPitchingChances` | Input from existing stat calculations |
---
## Contract System
A major enhancement: **pluggable placement strategies** that give cards different "personalities" using the same raw stats.
### Available Contracts
| Contract | Behavior | Use Case |
|----------|----------|----------|
| **Standard** | On-base results on middle rows (6-8) | Default behavior |
| **Clutch** | On-base results on edge rows (2-4, 10-12) | Role players, postseason heroes |
| **Power Heavy** | HR/3B/2B on prime rows, singles on edges | HR leaders, sluggers |
| **Contact First** | Singles on prime rows, power on edges | Leadoff hitters, high-AVG players |
| **Groundball Pitcher** | Groundouts on middle rows | Sinkerballers |
| **Flyball Pitcher** | Strikeouts/flyouts on middle rows | Power pitchers |
### Example: Same Stats, Different Cards
```
Power Heavy Contract:
Row 7 (most likely): HOMERUN
Row 12 (least likely): SINGLE
Contact First Contract:
Row 7 (most likely): SINGLE
Row 12 (least likely): HOMERUN
```
### Contract Interface
```python
class CardContract:
def get_row_preference(self, category: PlayCategory) -> List[int]
def get_play_priority(self) -> List[PlayCategory]
def should_use_splits(self, category: PlayCategory) -> bool
def max_splits_per_column(self) -> int
```
---
## Benefits
### Immediate
1. **Deterministic output** - What Python calculates is exactly what appears on the card
2. **No step 7** - Database doesn't modify/re-save values
3. **Local preview** - Validate cards without hitting the API
4. **Single source of truth** - Fitting logic in one place
### Strategic
1. **Card personalities** - Same player can have different "feels" via contracts
2. **Testable** - Unit test card building without database
3. **Extensible** - Add new contracts without touching core logic
4. **Data-driven** - Store contract name in DB, apply at generation time
---
## Migration Path
### Phase 1: Extract & Validate
- Extract current database fitting logic to shared module
- Run old and new in parallel, compare outputs
- Fix discrepancies
### Phase 2: Python Adoption
- Modify Python card-creation to use `CardBuilder`
- POST fitted card structures instead of raw chances
- Keep raw chances in separate field for debugging
### Phase 3: Database Simplification
- Database receives pre-fitted card structures
- Remove fitting logic from database
- Eliminate step 7 (no re-saving)
### Phase 4: Enhancements
- Add preview endpoint (returns structure without saving)
- Implement contract selection in card creation workflow
- Build custom contract UI
---
## Technical Details
### Discrete Probability Space
Valid chance values are defined by `EXACT_CHANCES`:
```python
[5.7, 5.4, 5.1, 4.8, 4.75, 4.5, 4.25, 4.2, 3.9, 3.8, 3.75, ...]
```
Plus whole numbers 1-6 (full row allocations).
### Pitching Card Specifics
- **X-checks**: Redirect to batter's card (e.g., `GB (ss) X`)
- **Batter Power**: `bp_homerun`, `bp_single` redirect to batter
- **Secondary plays**: D20 splits pair primary with complementary outcome
- **Bolding**: Strikeouts bolded (good for pitcher), hits not bolded
### Row Frequencies
```
Row: 2 3 4 5 6 7 8 9 10 11 12
Freq: 1 2 3 4 5 6 5 4 3 2 1
```
Row 7 is most valuable (6 chances = 16.67% of outcomes).
---
## Files Produced
| File | Description |
|------|-------------|
| `card_builder_sketch.py` | Main module: CardBuilder, contracts, data structures |
| `contracts.py` | Standalone contracts reference (superseded by integration) |
| `EXECUTIVE_SUMMARY.md` | This document |
---
## Open Questions
1. **Shared package or copy?** Extract to pip-installable package both repos import, or maintain synced copies?
2. **Keep raw chances?** Store both raw (for analysis) and fitted (for rendering), or just fitted?
3. **Contract storage** - Where to store contract selection? Player level? Cardset level? Both?
4. **Custom contracts** - Allow users to create custom contracts via UI?
---
## Recommendation
**Proceed with the shared card-building module approach.**
The contract system adds significant value by enabling card personalities without changing the underlying statistics. This addresses the original consistency problem while opening new design possibilities.
**Suggested next step:** Create a proof-of-concept by implementing `CardBuilder` against a single real player's stats and comparing output to current database behavior.
---
## Appendix: Code Location
All sketch files are in:
```
~/.claude/scratchpad/2026-01-22-card-builder-sketch/
```
To test:
```bash
cd ~/.claude/scratchpad/2026-01-22-card-builder-sketch
python card_builder_sketch.py
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,589 @@
"""
Card Building Contracts - Pluggable Placement Strategies
A "contract" defines the rules for how plays are placed on a card:
- Which rows to prefer for different play types
- Priority ordering of plays
- Secondary play pairing rules
- Split vs whole number preferences
- etc.
This enables different card "personalities" while using the same fitting engine.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import List, Dict, Optional, Callable, Tuple
# =============================================================================
# ROW PREFERENCE STRATEGIES
# =============================================================================
class RowPreference(Enum):
"""Built-in row ordering strategies."""
MIDDLE_FIRST = "middle_first" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12
EDGES_FIRST = "edges_first" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7
HIGH_TO_LOW = "high_to_low" # 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2
LOW_TO_HIGH = "low_to_high" # 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
SYMMETRIC_OUT = "symmetric_out" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12 (same as middle)
SYMMETRIC_IN = "symmetric_in" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7 (same as edges)
BY_FREQUENCY_DESC = "freq_desc" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12 (by chance count)
BY_FREQUENCY_ASC = "freq_asc" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7
ROW_ORDERINGS = {
RowPreference.MIDDLE_FIRST: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
RowPreference.EDGES_FIRST: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
RowPreference.HIGH_TO_LOW: [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2],
RowPreference.LOW_TO_HIGH: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
RowPreference.SYMMETRIC_OUT: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
RowPreference.SYMMETRIC_IN: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
RowPreference.BY_FREQUENCY_DESC: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
RowPreference.BY_FREQUENCY_ASC: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
}
# =============================================================================
# PLAY CATEGORY DEFINITIONS
# =============================================================================
class PlayCategory(Enum):
"""Categories of plays for contract rules."""
ON_BASE = "on_base" # Walks, HBP, singles, doubles, triples, HR
POWER = "power" # HR, triples, doubles
CONTACT = "contact" # Singles
PATIENCE = "patience" # Walks, HBP
STRIKEOUT = "strikeout"
GROUNDOUT = "groundout"
FLYOUT = "flyout"
XCHECK = "xcheck" # Pitcher X-checks
BATTER_POWER = "batter_power" # bp_homerun, bp_single
# =============================================================================
# THE CONTRACT INTERFACE
# =============================================================================
@dataclass
class PlacementRule:
"""A single rule for how to place a category of plays."""
category: PlayCategory
row_preference: RowPreference
priority: int = 50 # Lower = placed earlier (0-100)
prefer_splits: bool = True # Use d20 splits when possible?
max_per_column: Optional[int] = None # Limit instances per column
secondary_pool: List[str] = field(default_factory=list) # What can fill split remainder
class CardContract(ABC):
"""
Abstract base class for card building contracts.
A contract defines the "personality" of a card - how plays are distributed
across the 2d6 probability space.
"""
@property
@abstractmethod
def name(self) -> str:
"""Human-readable contract name."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Describe what this contract does."""
pass
@abstractmethod
def get_row_preference(self, category: PlayCategory) -> List[int]:
"""
Return ordered list of preferred rows for this play category.
First row in list is most preferred.
"""
pass
@abstractmethod
def get_play_priority(self) -> List[PlayCategory]:
"""
Return categories in priority order (placed first to last).
"""
pass
def get_secondary_pool(self, category: PlayCategory) -> List[PlayCategory]:
"""
Return categories that can fill d20 split remainders for this category.
Default: flyouts and groundouts.
"""
return [PlayCategory.FLYOUT, PlayCategory.GROUNDOUT]
def should_use_splits(self, category: PlayCategory) -> bool:
"""Whether this category should use d20 splits. Default: True."""
return True
def max_splits_per_column(self) -> int:
"""Maximum d20 splits allowed per column. Default: 4."""
return 4
def validate(self, card_data: dict) -> List[str]:
"""
Validate a built card against this contract's rules.
Returns list of violations (empty if valid).
"""
return []
# =============================================================================
# CONCRETE CONTRACT IMPLEMENTATIONS
# =============================================================================
class StandardContract(CardContract):
"""
The default contract - matches current database behavior.
- On-base results favor middle rows (more likely to occur)
- Outs fill remaining rows
- Standard priority ordering
"""
@property
def name(self) -> str:
return "Standard"
@property
def description(self) -> str:
return "Default card layout - on-base results on middle rows (6-8)"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category in [PlayCategory.ON_BASE, PlayCategory.POWER,
PlayCategory.CONTACT, PlayCategory.PATIENCE]:
# Good outcomes on likely rows
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
else:
# Outs on whatever's left (edges tend to be available)
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
return [
PlayCategory.BATTER_POWER, # bp_hr, bp_single first
PlayCategory.PATIENCE, # Walks, HBP
PlayCategory.XCHECK, # X-checks
PlayCategory.POWER, # HR, 3B, 2B
PlayCategory.CONTACT, # Singles
PlayCategory.STRIKEOUT,
PlayCategory.GROUNDOUT,
PlayCategory.FLYOUT,
]
class ClutchContract(CardContract):
"""
"Clutch" card personality - on-base results on EDGE rows.
This makes hits less likely overall, but when they happen,
the player "came through in the clutch" (rare but impactful).
Good for: Role players, clutch hitters, postseason heroes
"""
@property
def name(self) -> str:
return "Clutch"
@property
def description(self) -> str:
return "On-base results on edge rows (2-4, 10-12) - less frequent but memorable"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category in [PlayCategory.ON_BASE, PlayCategory.POWER,
PlayCategory.CONTACT, PlayCategory.PATIENCE]:
# Good outcomes on unlikely rows
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
else:
# Outs on middle rows (more common)
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
# Same priority, different placement
return StandardContract().get_play_priority()
class ConsistentContract(CardContract):
"""
"Consistent" card personality - spread results evenly.
Distributes on-base results across all rows proportionally,
making the player more predictable/reliable.
Good for: Contact hitters, batting average leaders
"""
@property
def name(self) -> str:
return "Consistent"
@property
def description(self) -> str:
return "Spread results evenly across all rows - predictable performance"
def get_row_preference(self, category: PlayCategory) -> List[int]:
# Alternate between middle and edges to spread out
return [7, 2, 8, 12, 6, 3, 9, 11, 5, 4, 10]
def get_play_priority(self) -> List[PlayCategory]:
return StandardContract().get_play_priority()
class PowerHeavyContract(CardContract):
"""
Power-focused card - extra-base hits get prime real estate.
HR and triples on row 7 (most likely), doubles on 6/8,
singles pushed to edges.
Good for: Power hitters, HR leaders
"""
@property
def name(self) -> str:
return "Power Heavy"
@property
def description(self) -> str:
return "Power hits (HR, 3B, 2B) on prime rows, singles on edges"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category == PlayCategory.POWER:
# Power on the most likely rows
return [7, 6, 8, 5, 9]
elif category == PlayCategory.CONTACT:
# Singles pushed to edges
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
elif category == PlayCategory.PATIENCE:
# Walks in the middle-ish
return [5, 9, 4, 10, 6, 8]
else:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
# Power comes before patience
return [
PlayCategory.BATTER_POWER,
PlayCategory.POWER, # HR, 3B, 2B FIRST
PlayCategory.PATIENCE, # Then walks
PlayCategory.XCHECK,
PlayCategory.CONTACT,
PlayCategory.STRIKEOUT,
PlayCategory.GROUNDOUT,
PlayCategory.FLYOUT,
]
class ContactFirstContract(CardContract):
"""
Contact-focused card - singles get prime placement.
Singles on middle rows, power hits on edges.
More base hits, but less extra-base damage.
Good for: Contact hitters, leadoff types, high-average players
"""
@property
def name(self) -> str:
return "Contact First"
@property
def description(self) -> str:
return "Singles on prime rows, power on edges - consistent base hits"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category == PlayCategory.CONTACT:
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
elif category == PlayCategory.POWER:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
elif category == PlayCategory.PATIENCE:
return [5, 9, 4, 10]
else:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
return [
PlayCategory.BATTER_POWER,
PlayCategory.CONTACT, # Singles FIRST
PlayCategory.PATIENCE,
PlayCategory.POWER,
PlayCategory.XCHECK,
PlayCategory.STRIKEOUT,
PlayCategory.GROUNDOUT,
PlayCategory.FLYOUT,
]
class GroundballPitcherContract(CardContract):
"""
Groundball pitcher personality.
Groundouts on middle rows, flyouts on edges.
X-checks to corner infielders get priority.
Good for: Sinkerballers, ground ball specialists
"""
@property
def name(self) -> str:
return "Groundball Pitcher"
@property
def description(self) -> str:
return "Groundouts on likely rows, X-checks favor corner infielders"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category == PlayCategory.GROUNDOUT:
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
elif category == PlayCategory.FLYOUT:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
elif category == PlayCategory.STRIKEOUT:
return [5, 9, 4, 10] # Mid-edges
else:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
return [
PlayCategory.BATTER_POWER,
PlayCategory.PATIENCE, # Walks/HBP
PlayCategory.XCHECK, # Corner IF X-checks
PlayCategory.GROUNDOUT, # Groundouts prioritized
PlayCategory.STRIKEOUT,
PlayCategory.POWER,
PlayCategory.CONTACT,
PlayCategory.FLYOUT,
]
class FlyballPitcherContract(CardContract):
"""
Flyball pitcher personality.
Flyouts and strikeouts on middle rows.
X-checks to outfielders get priority.
Good for: Power pitchers, high-K guys
"""
@property
def name(self) -> str:
return "Flyball Pitcher"
@property
def description(self) -> str:
return "Flyouts and strikeouts on likely rows, X-checks favor outfielders"
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category in [PlayCategory.FLYOUT, PlayCategory.STRIKEOUT]:
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
elif category == PlayCategory.GROUNDOUT:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
else:
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
def get_play_priority(self) -> List[PlayCategory]:
return [
PlayCategory.BATTER_POWER,
PlayCategory.PATIENCE,
PlayCategory.STRIKEOUT, # K's prioritized
PlayCategory.FLYOUT, # Flyouts prioritized
PlayCategory.XCHECK,
PlayCategory.POWER,
PlayCategory.CONTACT,
PlayCategory.GROUNDOUT,
]
# =============================================================================
# CUSTOM CONTRACT BUILDER
# =============================================================================
@dataclass
class CustomContract(CardContract):
"""
Build a custom contract from individual rules.
Example:
contract = CustomContract(
name="My Custom Card",
description="Power on edges, walks in middle",
rules={
PlayCategory.POWER: PlacementRule(
category=PlayCategory.POWER,
row_preference=RowPreference.EDGES_FIRST,
priority=10
),
PlayCategory.PATIENCE: PlacementRule(
category=PlayCategory.PATIENCE,
row_preference=RowPreference.MIDDLE_FIRST,
priority=5
),
}
)
"""
_name: str
_description: str
rules: Dict[PlayCategory, PlacementRule] = field(default_factory=dict)
default_row_preference: RowPreference = RowPreference.MIDDLE_FIRST
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
def get_row_preference(self, category: PlayCategory) -> List[int]:
if category in self.rules:
return ROW_ORDERINGS[self.rules[category].row_preference]
return ROW_ORDERINGS[self.default_row_preference]
def get_play_priority(self) -> List[PlayCategory]:
# Sort by priority value (lower = first)
sorted_rules = sorted(
self.rules.items(),
key=lambda x: x[1].priority
)
return [cat for cat, _ in sorted_rules]
def get_secondary_pool(self, category: PlayCategory) -> List[PlayCategory]:
if category in self.rules and self.rules[category].secondary_pool:
# Convert strings to PlayCategory if needed
return [PlayCategory(s) if isinstance(s, str) else s
for s in self.rules[category].secondary_pool]
return super().get_secondary_pool(category)
def should_use_splits(self, category: PlayCategory) -> bool:
if category in self.rules:
return self.rules[category].prefer_splits
return True
# =============================================================================
# CONTRACT REGISTRY
# =============================================================================
class ContractRegistry:
"""Registry of available contracts."""
_contracts: Dict[str, CardContract] = {}
@classmethod
def register(cls, contract: CardContract):
"""Register a contract by name."""
cls._contracts[contract.name.lower()] = contract
@classmethod
def get(cls, name: str) -> Optional[CardContract]:
"""Get a contract by name (case-insensitive)."""
return cls._contracts.get(name.lower())
@classmethod
def list_all(cls) -> List[str]:
"""List all registered contract names."""
return list(cls._contracts.keys())
@classmethod
def get_default(cls) -> CardContract:
"""Get the default contract."""
return StandardContract()
# Register built-in contracts
ContractRegistry.register(StandardContract())
ContractRegistry.register(ClutchContract())
ContractRegistry.register(ConsistentContract())
ContractRegistry.register(PowerHeavyContract())
ContractRegistry.register(ContactFirstContract())
ContractRegistry.register(GroundballPitcherContract())
ContractRegistry.register(FlyballPitcherContract())
# =============================================================================
# USAGE EXAMPLE
# =============================================================================
def example_contract_usage():
"""
How contracts would integrate with the CardBuilder.
"""
from card_builder_sketch import CardBuilder, RawBattingChances
# Get a contract
standard = ContractRegistry.get("standard")
clutch = ContractRegistry.get("clutch")
print("Available contracts:")
for name in ContractRegistry.list_all():
contract = ContractRegistry.get(name)
print(f" - {contract.name}: {contract.description}")
print("\nRow preferences for ON_BASE category:")
print(f" Standard: {standard.get_row_preference(PlayCategory.ON_BASE)[:5]}...")
print(f" Clutch: {clutch.get_row_preference(PlayCategory.ON_BASE)[:5]}...")
# Build a custom contract
custom = CustomContract(
_name="My Custom",
_description="Walks on edges, power in middle",
rules={
PlayCategory.PATIENCE: PlacementRule(
category=PlayCategory.PATIENCE,
row_preference=RowPreference.EDGES_FIRST,
priority=1
),
PlayCategory.POWER: PlacementRule(
category=PlayCategory.POWER,
row_preference=RowPreference.MIDDLE_FIRST,
priority=2
),
}
)
print(f"\nCustom contract rows for PATIENCE: {custom.get_row_preference(PlayCategory.PATIENCE)[:5]}...")
print(f"Custom contract rows for POWER: {custom.get_row_preference(PlayCategory.POWER)[:5]}...")
# =============================================================================
# INTEGRATION WITH CARD BUILDER
# =============================================================================
"""
To integrate contracts with CardBuilder, modify the builder to accept a contract:
class CardBuilder:
def __init__(self, contract: CardContract = None):
self.contract = contract or StandardContract()
self.max_splits = self.contract.max_splits_per_column()
def _get_available_rows(self, column, category: PlayCategory) -> List[int]:
# Instead of returning rows in default order,
# use the contract's preference for this category
preferred_order = self.contract.get_row_preference(category)
return [r for r in preferred_order if column.rows[r].is_empty]
def build_batting_card(self, ...):
# Use contract's priority order
for category in self.contract.get_play_priority():
plays = self._get_plays_for_category(category, chances)
for play, raw_chance in plays:
# Use contract's row preference when placing
available = self._get_available_rows(column, category)
...
This makes the fitting algorithm contract-aware without changing its core logic.
"""
if __name__ == '__main__':
example_contract_usage()