From 245a49c5cfc8759832a7078f758a799bbef6bc0b Mon Sep 17 00:00:00 2001 From: christopher harris Date: Wed, 12 Aug 2026 01:24:03 -0500 Subject: [PATCH] Record the probable starter, not the words "Probable Starting Pitcher" ESPN's probables entry carries both a slot label and the athlete. Its own displayName is the literal string "Probable Starting Pitcher", so reading that before the athlete meant the fallback never ran and every game resolved to the same constant. This was harmless while nothing persisted the field. #200 started persisting it, so the first live capture would have written one identical starter name onto every MLB game in the lake - with correct, varying ERAs beside it, which is exactly the shape that looks populated in a spot check. Pitcher identity is the entire point of the field: a per-pitcher rating keyed on a constant is one pooled number wearing 500 names. Caught by running the merged wire end to end against tomorrow's slate before trusting it; the rows came back with real ERAs and a constant name. The tests assert precedence rather than presence, because the label is always present - a test that merely checked for a non-empty name passes on the bug. Co-Authored-By: Claude Opus 5 --- autonomy/sports/espn.py | 7 +++++- tests/test_espn_lake_adapter.py | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/autonomy/sports/espn.py b/autonomy/sports/espn.py index e68a790..f3f921f 100644 --- a/autonomy/sports/espn.py +++ b/autonomy/sports/espn.py @@ -121,7 +121,12 @@ def _probable_era(competitor: dict[str, Any]) -> tuple[float | None, str | None] if not probs: return None, None p = probs[0] - name = p.get("displayName") or (p.get("athlete") or {}).get("displayName") + # The probables entry carries BOTH a slot label and the athlete. Its own + # ``displayName`` is the label -- literally "Probable Starting Pitcher" -- + # so reading it first meant the athlete fallback never ran and every game + # recorded the same constant string as its starter. Ask the athlete first. + athlete = p.get("athlete") or {} + name = athlete.get("displayName") or athlete.get("fullName") or p.get("displayName") for stat in p.get("statistics") or []: if str(stat.get("name", "")).lower() in ("era", "earnedrunaverage"): try: diff --git a/tests/test_espn_lake_adapter.py b/tests/test_espn_lake_adapter.py index 84350f2..d18d80e 100644 --- a/tests/test_espn_lake_adapter.py +++ b/tests/test_espn_lake_adapter.py @@ -214,3 +214,41 @@ def test_probable_pitcher_fields_are_omitted_not_null_when_absent(tmp_path): (row,) = espn_games_to_rows([game], source="espn", received_at="2026-08-12T18:00:00Z") assert "home_probable_pitcher" not in row["extra"] assert "away_probable_pitcher" not in row["extra"] + + +def test_probable_starter_is_the_athlete_not_the_slot_label(): + """ESPN's probables entry carries a label AND an athlete; take the athlete. + + The entry's own ``displayName`` is the literal string "Probable Starting + Pitcher". Reading it first meant the athlete fallback never ran and every + game in the lake recorded the same constant as its starter -- ERAs varied + correctly, so the rows looked populated while pitcher identity, the whole + point of the field, was uniform garbage. The assertion is on precedence, + because the label is always present. + """ + + from autonomy.sports.espn import _probable_era + + era, name = _probable_era( + { + "probables": [ + { + "name": "probableStartingPitcher", + "displayName": "Probable Starting Pitcher", + "athlete": {"displayName": "Jameson Taillon", "fullName": "Jameson Taillon"}, + "statistics": [{"name": "ERA", "displayValue": "2.87"}], + } + ] + } + ) + assert name == "Jameson Taillon" + assert era == 2.87 + + +def test_probable_starter_falls_back_to_the_label_only_when_no_athlete(): + from autonomy.sports.espn import _probable_era + + _, name = _probable_era( + {"probables": [{"displayName": "Probable Starting Pitcher", "statistics": []}]} + ) + assert name == "Probable Starting Pitcher"