strat-gameplay-webapp/.claude/implementation/phase-3b-league-config-tables.md
Cal Corum 0b6076d5b8 CLAUDE: Implement Phase 3B - X-Check league config tables
Complete X-Check resolution table system for defensive play outcomes.

Components:
- Defense range tables (20×5) for infield, outfield, catcher
- Error charts for LF/RF and CF (ratings 0-25)
- Placeholder error charts for P, C, 1B, 2B, 3B, SS (awaiting data)
- get_fielders_holding_runners() - Complete implementation
- get_error_chart_for_position() - Maps all 9 positions
- 6 X-Check placeholder advancement functions (g1-g3, f1-f3)

League Config Integration:
- Both SbaConfig and PdConfig include X-Check tables
- Shared common tables via league_configs.py
- Attributes: x_check_defense_tables, x_check_error_charts, x_check_holding_runners

Testing:
- 36 tests for X-Check tables (all passing)
- 9 tests for X-Check placeholders (all passing)
- Total: 45/45 tests passing

Documentation:
- Updated backend/CLAUDE.md with Phase 3B section
- Updated app/config/CLAUDE.md with X-Check tables documentation
- Updated app/core/CLAUDE.md with X-Check placeholder functions
- Updated tests/CLAUDE.md with new test counts (519 unit tests)
- Updated phase-3b-league-config-tables.md (marked complete)
- Updated NEXT_SESSION.md with Phase 3B completion

What's Pending:
- 6 infield error charts need actual data (P, C, 1B, 2B, 3B, SS)
- Phase 3C will implement full X-Check resolution logic

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 19:50:55 -05:00

478 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 3B: League Config Tables for X-Check Resolution
**Status**: ✅ Complete
**Completed**: 2025-11-01
**Actual Effort**: 3 hours
**Dependencies**: Phase 3A (Data Models)
## Overview
Create defense tables, error charts, and placeholder advancement tables for X-Check resolution. These tables are used to convert dice rolls into play outcomes.
Tables are stored in league configs with shared common tables imported by both SBA and PD configs.
## Tasks
### 1. Create Common X-Check Tables Module
**File**: `backend/app/config/common_x_check_tables.py` (NEW FILE)
```python
"""
Common X-Check resolution tables shared across SBA and PD leagues.
Tables include:
- Defense range tables (20x5) for each position type
- Error charts mapping 3d6 rolls to error types
- Holding runner responsibility chart
Author: Claude
Date: 2025-11-01
"""
from typing import List, Tuple
# ============================================================================
# DEFENSE RANGE TABLES (1d20 × Defense Range 1-5)
# ============================================================================
# Row index = d20 roll - 1 (0-indexed)
# Column index = defense range - 1 (0-indexed)
# Values = base result code (G1, SI2, F2, etc.)
INFIELD_DEFENSE_TABLE: List[List[str]] = [
# Range: 1 2 3 4 5
# Best Good Avg Poor Worst
['G3#', 'SI1', 'SI2', 'SI2', 'SI2'], # d20 = 1
['G2#', 'SI1', 'SI2', 'SI2', 'SI2'], # d20 = 2
['G2#', 'G3#', 'SI1', 'SI2', 'SI2'], # d20 = 3
['G2#', 'G3#', 'SI1', 'SI2', 'SI2'], # d20 = 4
['G1', 'G3#', 'G3#', 'SI1', 'SI2'], # d20 = 5
['G1', 'G2#', 'G3#', 'SI1', 'SI2'], # d20 = 6
['G1', 'G2', 'G3#', 'G3#', 'SI1'], # d20 = 7
['G1', 'G2', 'G3#', 'G3#', 'SI1'], # d20 = 8
['G1', 'G2', 'G3', 'G3#', 'G3#'], # d20 = 9
['G1', 'G1', 'G2', 'G3#', 'G3#'], # d20 = 10
['G1', 'G1', 'G2', 'G3', 'G3#'], # d20 = 11
['G1', 'G1', 'G2', 'G3', 'G3#'], # d20 = 12
['G1', 'G1', 'G2', 'G3', 'G3'], # d20 = 13
['G1', 'G1', 'G2', 'G2', 'G3'], # d20 = 14
['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 15
['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 16
['G1', 'G1', 'G1', 'G1', 'G3'], # d20 = 17
['G1', 'G1', 'G1', 'G1', 'G2'], # d20 = 18
['G1', 'G1', 'G1', 'G1', 'G2'], # d20 = 19
['G1', 'G1', 'G1', 'G1', 'G1'], # d20 = 20
]
OUTFIELD_DEFENSE_TABLE: List[List[str]] = [
# Range: 1 2 3 4 5
# Best Good Avg Poor Worst
['TR3', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 1
['DO3', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 2
['DO2', 'DO3', 'DO3', 'DO3', 'DO3'], # d20 = 3
['DO2', 'DO2', 'DO3', 'DO3', 'DO3'], # d20 = 4
['SI2', 'DO2', 'DO2', 'DO3', 'DO3'], # d20 = 5
['SI2', 'SI2', 'DO2', 'DO2', 'DO3'], # d20 = 6
['F1', 'SI2', 'SI2', 'DO2', 'DO2'], # d20 = 7
['F1', 'F1', 'SI2', 'SI2', 'DO2'], # d20 = 8
['F1', 'F1', 'F1', 'SI2', 'SI2'], # d20 = 9
['F1', 'F1', 'F1', 'SI2', 'SI2'], # d20 = 10
['F1', 'F1', 'F1', 'F1', 'SI2'], # d20 = 11
['F1', 'F1', 'F1', 'F1', 'SI2'], # d20 = 12
['F1', 'F1', 'F1', 'F1', 'F1'], # d20 = 13
['F2', 'F1', 'F1', 'F1', 'F1'], # d20 = 14
['F2', 'F2', 'F1', 'F1', 'F1'], # d20 = 15
['F2', 'F2', 'F2', 'F1', 'F1'], # d20 = 16
['F2', 'F2', 'F2', 'F2', 'F1'], # d20 = 17
['F3', 'F2', 'F2', 'F2', 'F2'], # d20 = 18
['F3', 'F3', 'F2', 'F2', 'F2'], # d20 = 19
['F3', 'F3', 'F3', 'F2', 'F2'], # d20 = 20
]
CATCHER_DEFENSE_TABLE: List[List[str]] = [
# Range: 1 2 3 4 5
# Best Good Avg Poor Worst
['G3', 'SI1', 'SI1', 'SI1', 'SI1'], # d20 = 1
['G2', 'G3', 'SI1', 'SI1', 'SI1'], # d20 = 2
['G2', 'G3', 'SI1', 'SI1', 'SI1'], # d20 = 3
['G1', 'G2', 'G3', 'SI1', 'SI1'], # d20 = 4
['G1', 'G2', 'G3', 'SI1', 'SI1'], # d20 = 5
['G1', 'G1', 'G2', 'G3', 'SI1'], # d20 = 6
['G1', 'G1', 'G2', 'G3', 'SI1'], # d20 = 7
['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 8
['G1', 'G1', 'G1', 'G2', 'G3'], # d20 = 9
['SPD', 'G1', 'G1', 'G1', 'G2'], # d20 = 10
['SPD', 'SPD', 'G1', 'G1', 'G1'], # d20 = 11
['SPD', 'SPD', 'SPD', 'G1', 'G1'], # d20 = 12
['FO', 'SPD', 'SPD', 'SPD', 'G1'], # d20 = 13
['FO', 'FO', 'SPD', 'SPD', 'SPD'], # d20 = 14
['FO', 'FO', 'FO', 'SPD', 'SPD'], # d20 = 15
['PO', 'FO', 'FO', 'FO', 'SPD'], # d20 = 16
['PO', 'PO', 'FO', 'FO', 'FO'], # d20 = 17
['PO', 'PO', 'PO', 'FO', 'FO'], # d20 = 18
['PO', 'PO', 'PO', 'PO', 'FO'], # d20 = 19
['PO', 'PO', 'PO', 'PO', 'PO'], # d20 = 20
]
# ============================================================================
# ERROR CHARTS (3d6 totals by Error Rating and Position Type)
# ============================================================================
# Structure: {error_rating: {'RP': [rolls], 'E1': [rolls], 'E2': [rolls], 'E3': [rolls]}}
# If 3d6 sum is in the list for that error rating, apply that error type
# Otherwise, error_result = 'NO' (no error)
# Corner Outfield (LF, RF) Error Chart
LF_RF_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [3], 'E2': [], 'E3': [17]},
2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [16]},
3: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [15]},
4: {'RP': [5], 'E1': [4], 'E2': [3, 15], 'E3': [18]},
5: {'RP': [5], 'E1': [4], 'E2': [14], 'E3': [18]},
6: {'RP': [5], 'E1': [3, 4], 'E2': [14, 17], 'E3': [18]},
7: {'RP': [5], 'E1': [16], 'E2': [6, 15], 'E3': [18]},
8: {'RP': [5], 'E1': [16], 'E2': [6, 15, 17], 'E3': [3, 18]},
9: {'RP': [5], 'E1': [16], 'E2': [11], 'E3': [3, 18]},
10: {'RP': [5], 'E1': [4, 16], 'E2': [14, 15, 17], 'E3': [3, 18]},
11: {'RP': [5], 'E1': [4, 16], 'E2': [13, 15], 'E3': [3, 18]},
12: {'RP': [5], 'E1': [4, 16], 'E2': [13, 14], 'E3': [3, 18]},
13: {'RP': [5], 'E1': [6], 'E2': [4, 12, 15], 'E3': [17]},
14: {'RP': [5], 'E1': [3, 6], 'E2': [12, 14], 'E3': [17]},
15: {'RP': [5], 'E1': [3, 6, 18], 'E2': [4, 12, 14], 'E3': [17]},
16: {'RP': [5], 'E1': [4, 6], 'E2': [12, 13], 'E3': [17]},
17: {'RP': [5], 'E1': [4, 6], 'E2': [9, 12], 'E3': [17]},
18: {'RP': [5], 'E1': [3, 4, 6], 'E2': [11, 12, 18], 'E3': [17]},
19: {'RP': [5], 'E1': [7], 'E2': [3, 10, 11], 'E3': [17, 18]},
20: {'RP': [5], 'E1': [3, 7], 'E2': [11, 13, 16], 'E3': [17, 18]},
21: {'RP': [5], 'E1': [3, 7], 'E2': [11, 12, 15], 'E3': [17, 18]},
22: {'RP': [5], 'E1': [3, 6, 16], 'E2': [9, 12, 14], 'E3': [17, 18]},
23: {'RP': [5], 'E1': [4, 7], 'E2': [11, 12, 14], 'E3': [17, 18]},
24: {'RP': [5], 'E1': [4, 6, 16], 'E2': [8, 11, 13], 'E3': [3, 17, 18]},
25: {'RP': [5], 'E1': [4, 6, 16], 'E2': [11, 12, 13], 'E3': [3, 17, 18]},
}
# Center Field Error Chart
CF_ERROR_CHART: dict[int, dict[str, List[int]]] = {
0: {'RP': [5], 'E1': [], 'E2': [], 'E3': []},
1: {'RP': [5], 'E1': [3], 'E2': [], 'E3': [17]},
2: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [16]},
3: {'RP': [5], 'E1': [3, 18], 'E2': [], 'E3': [15]},
4: {'RP': [5], 'E1': [4], 'E2': [3, 15], 'E3': [18]},
5: {'RP': [5], 'E1': [4], 'E2': [14], 'E3': [18]},
6: {'RP': [5], 'E1': [3, 4], 'E2': [14, 17], 'E3': [18]},
7: {'RP': [5], 'E1': [16], 'E2': [6, 15], 'E3': [18]},
8: {'RP': [5], 'E1': [16], 'E2': [6, 15, 17], 'E3': [3, 18]},
9: {'RP': [5], 'E1': [16], 'E2': [11], 'E3': [3, 18]},
10: {'RP': [5], 'E1': [4, 16], 'E2': [14, 15, 17], 'E3': [3, 18]},
11: {'RP': [5], 'E1': [4, 16], 'E2': [13, 15], 'E3': [3, 18]},
12: {'RP': [5], 'E1': [4, 16], 'E2': [13, 14], 'E3': [3, 18]},
13: {'RP': [5], 'E1': [6], 'E2': [4, 12, 15], 'E3': [17]},
14: {'RP': [5], 'E1': [3, 6], 'E2': [12, 14], 'E3': [17]},
15: {'RP': [5], 'E1': [3, 6, 18], 'E2': [4, 12, 14], 'E3': [17]},
16: {'RP': [5], 'E1': [4, 6], 'E2': [12, 13], 'E3': [17]},
17: {'RP': [5], 'E1': [4, 6], 'E2': [9, 12], 'E3': [17]},
18: {'RP': [5], 'E1': [3, 4, 6], 'E2': [11, 12, 18], 'E3': [17]},
19: {'RP': [5], 'E1': [7], 'E2': [3, 10, 11], 'E3': [17, 18]},
20: {'RP': [5], 'E1': [3, 7], 'E2': [11, 13, 16], 'E3': [17, 18]},
21: {'RP': [5], 'E1': [3, 7], 'E2': [11, 12, 15], 'E3': [17, 18]},
22: {'RP': [5], 'E1': [3, 6, 16], 'E2': [9, 12, 14], 'E3': [17, 18]},
23: {'RP': [5], 'E1': [4, 7], 'E2': [11, 12, 14], 'E3': [17, 18]},
24: {'RP': [5], 'E1': [4, 6, 16], 'E2': [8, 11, 13], 'E3': [3, 17, 18]},
25: {'RP': [5], 'E1': [4, 6, 16], 'E2': [11, 12, 13], 'E3': [3, 17, 18]},
}
# Infield Error Charts
# TODO: Add P, C, 1B, 2B, 3B, SS error charts
# Structure same as OF charts above
# Placeholder for now - to be filled with actual data
PITCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
CATCHER_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
FIRST_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
SECOND_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
THIRD_BASE_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
SHORTSTOP_ERROR_CHART: dict[int, dict[str, List[int]]] = {} # TODO
# ============================================================================
# HOLDING RUNNER RESPONSIBILITY CHART
# ============================================================================
def get_fielders_holding_runners(
runner_bases: List[int],
batter_handedness: str
) -> List[str]:
"""
Determine which fielders are responsible for holding runners.
Used to determine if G2#/G3# results should convert to SI2.
Args:
runner_bases: List of bases with runners (e.g., [1, 3] for R1 and R3)
batter_handedness: 'L' or 'R'
Returns:
List of position codes responsible for holds (e.g., ['1B', 'SS'])
TODO: Implement full chart logic when chart is provided
For now, simple heuristic:
- R1 only: 1B holds
- R1 + others: 2B or SS holds depending on handedness
- R2 only: No holds
- R3 only: No holds
"""
if not runner_bases:
return []
holding_positions = []
if 1 in runner_bases:
# Runner on first
if len(runner_bases) == 1:
# Only R1
holding_positions.append('1B')
else:
# R1 + others - middle infielder holds
if batter_handedness == 'R':
holding_positions.append('SS')
else:
holding_positions.append('2B')
return holding_positions
# ============================================================================
# ERROR CHART LOOKUP HELPER
# ============================================================================
def get_error_chart_for_position(position: str) -> dict[int, dict[str, List[int]]]:
"""
Get error chart for a specific position.
Args:
position: Position code (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
Returns:
Error chart dict
Raises:
ValueError: If position not recognized
"""
charts = {
'P': PITCHER_ERROR_CHART,
'C': CATCHER_ERROR_CHART,
'1B': FIRST_BASE_ERROR_CHART,
'2B': SECOND_BASE_ERROR_CHART,
'3B': THIRD_BASE_ERROR_CHART,
'SS': SHORTSTOP_ERROR_CHART,
'LF': LF_RF_ERROR_CHART,
'RF': LF_RF_ERROR_CHART,
'CF': CF_ERROR_CHART,
}
if position not in charts:
raise ValueError(f"Unknown position: {position}")
return charts[position]
```
### 2. Import Common Tables in League Configs
**File**: `backend/app/config/sba_config.py`
**Add imports**:
```python
from app.config.common_x_check_tables import (
INFIELD_DEFENSE_TABLE,
OUTFIELD_DEFENSE_TABLE,
CATCHER_DEFENSE_TABLE,
LF_RF_ERROR_CHART,
CF_ERROR_CHART,
get_fielders_holding_runners,
get_error_chart_for_position,
)
# Use common tables (no overrides for SBA)
X_CHECK_DEFENSE_TABLES = {
'infield': INFIELD_DEFENSE_TABLE,
'outfield': OUTFIELD_DEFENSE_TABLE,
'catcher': CATCHER_DEFENSE_TABLE,
}
X_CHECK_ERROR_CHARTS = get_error_chart_for_position # Use common function
```
**File**: `backend/app/config/pd_config.py`
**Add same imports** (for now, PD uses common tables):
```python
from app.config.common_x_check_tables import (
INFIELD_DEFENSE_TABLE,
OUTFIELD_DEFENSE_TABLE,
CATCHER_DEFENSE_TABLE,
LF_RF_ERROR_CHART,
CF_ERROR_CHART,
get_fielders_holding_runners,
get_error_chart_for_position,
)
X_CHECK_DEFENSE_TABLES = {
'infield': INFIELD_DEFENSE_TABLE,
'outfield': OUTFIELD_DEFENSE_TABLE,
'catcher': CATCHER_DEFENSE_TABLE,
}
X_CHECK_ERROR_CHARTS = get_error_chart_for_position
```
### 3. Add Placeholder Runner Advancement Functions
**File**: `backend/app/core/runner_advancement.py`
**Add at end of file** (placeholders for Phase 3D):
```python
# ============================================================================
# X-CHECK RUNNER ADVANCEMENT (Placeholders - to be implemented in Phase 3D)
# ============================================================================
def x_check_g1(
on_base_code: int,
defender_in: bool,
error_result: str
) -> AdvancementResult:
"""
Runner advancement for X-Check G1 result.
TODO: Implement full table lookups in Phase 3D
Args:
on_base_code: Current base situation code
defender_in: Is the defender playing in?
error_result: 'NO', 'E1', 'E2', 'E3', 'RP'
Returns:
AdvancementResult with runner movements
"""
# Placeholder
return AdvancementResult(movements=[], requires_decision=False)
def x_check_g2(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
"""X-Check G2 advancement (TODO: Phase 3D)."""
return AdvancementResult(movements=[], requires_decision=False)
def x_check_g3(on_base_code: int, defender_in: bool, error_result: str) -> AdvancementResult:
"""X-Check G3 advancement (TODO: Phase 3D)."""
return AdvancementResult(movements=[], requires_decision=False)
def x_check_f1(on_base_code: int, error_result: str) -> AdvancementResult:
"""X-Check F1 advancement (TODO: Phase 3D)."""
return AdvancementResult(movements=[], requires_decision=False)
def x_check_f2(on_base_code: int, error_result: str) -> AdvancementResult:
"""X-Check F2 advancement (TODO: Phase 3D)."""
return AdvancementResult(movements=[], requires_decision=False)
def x_check_f3(on_base_code: int, error_result: str) -> AdvancementResult:
"""X-Check F3 advancement (TODO: Phase 3D)."""
return AdvancementResult(movements=[], requires_decision=False)
# Add more placeholders for SI1, SI2, DO2, DO3, TR3, FO, PO as needed
```
## Testing Requirements
1. **Unit Tests**: `tests/config/test_x_check_tables.py`
- Test defense table dimensions (20 rows × 5 columns)
- Test error chart structure
- Test get_error_chart_for_position()
- Test get_fielders_holding_runners() with various scenarios
2. **Unit Tests**: `tests/core/test_runner_advancement.py`
- Test placeholder functions return valid AdvancementResult
- Verify function signatures
## Acceptance Criteria
- [x] ✅ common_x_check_tables.py created with all defense tables
- [x] ✅ LF/RF and CF error charts complete (ratings 0-25)
- [x] ✅ Placeholder error charts for P, C, 1B, 2B, 3B, SS (empty dicts ready for data)
- [x] ✅ get_fielders_holding_runners() implemented with complete logic
- [x] ✅ get_error_chart_for_position() implemented
- [x] ✅ SBA and PD configs import common tables (via league_configs.py)
- [x] ✅ Placeholder advancement functions added to runner_advancement.py (6 functions)
- [x] ✅ All unit tests pass (45/45 tests passing)
- [x] ✅ No import errors
## Implementation Notes
### What Was Completed
1. **Defense Range Tables** (100% Complete)
- `INFIELD_DEFENSE_TABLE`: 20×5 table with all result codes (G1, G2, G2#, G3, G3#, SI1, SI2)
- `OUTFIELD_DEFENSE_TABLE`: 20×5 table with all result codes (F1, F2, F3, SI2, DO2, DO3, TR3)
- `CATCHER_DEFENSE_TABLE`: 20×5 table with all result codes (G1, G2, G3, SI1, SPD, FO, PO)
2. **Error Charts** (Partial - Outfield Complete)
- `LF_RF_ERROR_CHART`: Complete with all 26 ratings (0-25) and error type distributions
- `CF_ERROR_CHART`: Complete with all 26 ratings (0-25) and error type distributions
- Infield charts (P, C, 1B, 2B, 3B, SS): Empty dict placeholders ready for data
3. **Helper Functions** (100% Complete)
- `get_fielders_holding_runners()`: Full implementation tracking all runners by base
- R1: 1B + middle infielder (2B for RHB, SS for LHB)
- R2: Middle infielder (2B for RHB, SS for LHB) if not already added
- R3: 3B holds
- `get_error_chart_for_position()`: Complete with all 9 positions mapped
4. **League Config Integration** (100% Complete)
- Tables imported in `league_configs.py` (not separate sba_config.py/pd_config.py as originally planned)
- Both SbaConfig and PdConfig have `x_check_defense_tables`, `x_check_error_charts`, `x_check_holding_runners` attributes
- Shared common tables for both leagues
5. **Placeholder Advancement Functions** (100% Complete)
- 6 functions implemented: `x_check_g1`, `x_check_g2`, `x_check_g3`, `x_check_f1`, `x_check_f2`, `x_check_f3`
- All return valid `AdvancementResult` structures
- Ready for Phase 3D implementation
6. **Test Coverage** (100% Complete)
- 36 tests for X-Check tables (defense tables, error charts, helpers, integration)
- 9 tests for placeholder advancement functions
- **Total: 45/45 tests passing**
### What Still Needs Data
**Infield Error Charts** - 6 positions awaiting actual data:
- `PITCHER_ERROR_CHART`
- `CATCHER_ERROR_CHART`
- `FIRST_BASE_ERROR_CHART`
- `SECOND_BASE_ERROR_CHART`
- `THIRD_BASE_ERROR_CHART`
- `SHORTSTOP_ERROR_CHART`
Each needs the same structure as outfield charts:
```python
{
0: {'RP': [rolls], 'E1': [rolls], 'E2': [rolls], 'E3': [rolls]},
# ... ratings 1-25
}
```
### Deviations from Original Plan
1. **Config File Structure**: Used unified `league_configs.py` instead of separate `sba_config.py` and `pd_config.py` files (these don't exist in current architecture)
2. **Holding Runners Implementation**: Completed with full logic instead of placeholder heuristic - tracks all runners by base position
3. **Advancement Function Signatures**: Updated to match actual `AdvancementResult` structure (no `requires_decision` parameter)
## Next Phase
After infield error chart data is provided, proceed to **Phase 3C: X-Check Resolution Logic**