diff --git a/autonomy/ingest/espn_lake.py b/autonomy/ingest/espn_lake.py index 87c1193..fdb0508 100644 --- a/autonomy/ingest/espn_lake.py +++ b/autonomy/ingest/espn_lake.py @@ -79,6 +79,21 @@ def espn_games_to_rows( "away_ml_open": getattr(g, "away_ml_open", None), "odds_provider": getattr(g, "odds_provider", None), } + # Probable starters: EspnClient has parsed these since the ERA work, + # but the lake never wrote them, so the point-in-time pitcher record + # was dropped on the floor each night. Only a pre-game observation is + # a *probable* -- once the game starts ESPN's field describes who DID + # start, which is settlement fact wearing a forecast's name -- so the + # write is gated on status. History cannot be backfilled; this wire + # only pays going forward, which is exactly why it must not stay cut. + if status == "scheduled": + for side in ("home", "away"): + pitcher = getattr(g, f"{side}_pitcher", None) + if pitcher: + extra[f"{side}_probable_pitcher"] = pitcher + era = getattr(g, f"{side}_pitcher_era", None) + if era is not None: + extra[f"{side}_probable_pitcher_era"] = era if basis: extra["availability_basis"] = basis rows.append({ diff --git a/tests/test_espn_lake_adapter.py b/tests/test_espn_lake_adapter.py index 7868e48..84350f2 100644 --- a/tests/test_espn_lake_adapter.py +++ b/tests/test_espn_lake_adapter.py @@ -146,3 +146,71 @@ def games(self, league, dates=None): assert res["ok"] is False and store.games() == [] assert store.last_ingest("espn", "nhl")["status"].startswith("error") store.close() + + +def test_probable_pitchers_land_in_extra_only_before_first_pitch(tmp_path): + """The client has parsed probables for months; the lake dropped them. + + A pre-game observation is a probable. The same field on an in-progress or + final game describes who DID start - settlement fact wearing a forecast's + name - so it must not be written as if it had been knowable in advance. + """ + + scheduled = Game( + game_id="401", + league="mlb", + home="NYY", + away="BOS", + status="pre", + home_won=None, + date="2026-08-12T23:05:00Z", + home_pitcher="Gerrit Cole", + away_pitcher="Brayan Bello", + home_pitcher_era=2.87, + away_pitcher_era=4.12, + ) + started = Game( + game_id="402", + league="mlb", + home="LAD", + away="CHC", + status="in", + home_won=None, + date="2026-08-12T20:05:00Z", + home_pitcher="Tyler Glasnow", + away_pitcher="Shota Imanaga", + home_pitcher_era=3.05, + away_pitcher_era=2.61, + ) + rows = espn_games_to_rows( + [scheduled, started], source="espn", received_at="2026-08-12T18:00:00Z" + ) + by_id = {row["game_id"]: row for row in rows} + + pre = by_id["401"]["extra"] + assert pre["home_probable_pitcher"] == "Gerrit Cole" + assert pre["away_probable_pitcher"] == "Brayan Bello" + assert pre["home_probable_pitcher_era"] == 2.87 + assert pre["away_probable_pitcher_era"] == 4.12 + + live = by_id["402"]["extra"] + assert "home_probable_pitcher" not in live + assert "away_probable_pitcher" not in live + assert "home_probable_pitcher_era" not in live + + +def test_probable_pitcher_fields_are_omitted_not_null_when_absent(tmp_path): + """Leagues without probables must not grow four null keys per game.""" + + game = Game( + game_id="403", + league="wnba", + home="LVA", + away="NYL", + status="pre", + home_won=None, + date="2026-08-12T23:00:00Z", + ) + (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"]