diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1e6f0b3..853d28f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(ls:*)", "Bash(xcrun simctl install:*)", "Skill(frontend-design:frontend-design)", - "Bash(xcrun simctl io:*)" + "Bash(xcrun simctl io:*)", + "Bash(python cloudkit_import.py:*)" ] } } diff --git a/Scripts/Simulator Screenshot - iPhone 17 Pro - 2026-01-07 at 17.37.41.png b/Scripts/Simulator Screenshot - iPhone 17 Pro - 2026-01-07 at 17.37.41.png deleted file mode 100644 index 16469b1..0000000 Binary files a/Scripts/Simulator Screenshot - iPhone 17 Pro - 2026-01-07 at 17.37.41.png and /dev/null differ diff --git a/Scripts/data/pipeline_report.json b/Scripts/data/pipeline_report.json deleted file mode 100644 index d141905..0000000 --- a/Scripts/data/pipeline_report.json +++ /dev/null @@ -1,1620 +0,0 @@ -{ - "generated_at": "2026-01-06T10:34:06.978950", - "season": 2025, - "sport": "all", - "summary": { - "games_scraped": 5097, - "stadiums_scraped": 92, - "games_by_sport": { - "NBA": 1321, - "MLB": 2464, - "NHL": 1312 - }, - "high_severity": 33, - "medium_severity": 122, - "low_severity": 30 - }, - "game_validations": [ - { - "sport": "MLB", - "season": "2025", - "sources": [ - "statsapi.mlb.com", - "baseball-reference" - ], - "total_games_source1": 2464, - "total_games_source2": 2556, - "games_matched": 2402, - "games_missing_source1": 126, - "games_missing_source2": 29, - "discrepancies": [ - { - "game_key": "2025-10-04_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-03-22_MIL_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Athletics", - "severity": "medium" - }, - { - "game_key": "2025-10-06_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Philadelphia Phillies", - "severity": "medium" - }, - { - "game_key": "2025-10-04_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-07-01_DET_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Detroit Tigers @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_CIN_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-10-20_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-23_NYY_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-06-18_CHW_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Chicago White Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-22_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "medium" - }, - { - "game_key": "2025-03-21_KCR_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Athletics", - "severity": "medium" - }, - { - "game_key": "2025-10-14_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-03-22_NYY_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-03-24_ARI_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Arizona D'Backs", - "severity": "medium" - }, - { - "game_key": "2025-03-22_BOS_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ Tampa Bay Rays", - "severity": "medium" - }, - { - "game_key": "2025-03-23_HOU_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ Washington Nationals", - "severity": "medium" - }, - { - "game_key": "2025-03-24_BAL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Washington Nationals", - "severity": "medium" - }, - { - "game_key": "2025-03-23_DET_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Philadelphia Phillies", - "severity": "medium" - }, - { - "game_key": "2025-03-22_MIA_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "St. Louis Cardinals @ Miami Marlins", - "severity": "medium" - }, - { - "game_key": "2025-03-25_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "medium" - }, - { - "game_key": "2025-03-23_ARI_CHW", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ Chicago White Sox", - "severity": "medium" - }, - { - "game_key": "2025-05-22_BAL_BOS", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Baltimore Orioles @ Boston Red Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_CHW_CIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago White Sox @ Cincinnati Reds", - "severity": "medium" - }, - { - "game_key": "2025-10-13_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-10-28_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-03-24_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Los Angeles Angels", - "severity": "medium" - }, - { - "game_key": "2025-10-02_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "medium" - }, - { - "game_key": "2025-03-25_MIA_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Miami Marlins", - "severity": "medium" - }, - { - "game_key": "2025-06-04_KCR_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Kansas City Royals @ St. Louis Cardinals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-01_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-03-20_SDP_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Texas Rangers @ San Diego Padres", - "severity": "medium" - }, - { - "game_key": "2025-03-20_ARI_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Arizona D'Backs", - "severity": "medium" - }, - { - "game_key": "2025-10-05_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-22_MIN_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Minnesota Twins", - "severity": "medium" - }, - { - "game_key": "2025-10-24_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-24_SDP_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ San Diego Padres", - "severity": "medium" - }, - { - "game_key": "2025-09-30_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-03-21_DET_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Detroit Tigers", - "severity": "medium" - }, - { - "game_key": "2025-03-22_CIN_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ San Diego Padres", - "severity": "medium" - }, - { - "game_key": "2025-06-18_DET_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Pittsburgh Pirates @ Detroit Tigers", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-09-30_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "high" - }, - { - "game_key": "2025-04-18_COL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Washington Nationals @ Colorado Rockies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-25_ATL_CHC", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-03-23_STL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Washington Nationals @ St. Louis Cardinals", - "severity": "medium" - }, - { - "game_key": "2025-03-20_TBR_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-24_NYM_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ New York Mets", - "severity": "medium" - }, - { - "game_key": "2025-07-01_MIL_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Milwaukee Brewers @ New York Mets", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-20_ATL_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Atlanta Braves", - "severity": "medium" - }, - { - "game_key": "2025-03-21_HOU_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ St. Louis Cardinals", - "severity": "medium" - }, - { - "game_key": "2025-03-23_BOS_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Minnesota Twins @ Boston Red Sox", - "severity": "medium" - }, - { - "game_key": "2025-03-23_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-10-04_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-10-04_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Philadelphia Phillies", - "severity": "medium" - }, - { - "game_key": "2025-03-23_ARI_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Arizona D'Backs", - "severity": "medium" - }, - { - "game_key": "2025-03-20_BOS_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ Minnesota Twins", - "severity": "medium" - }, - { - "game_key": "2025-03-23_MIA_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ New York Mets", - "severity": "medium" - }, - { - "game_key": "2025-07-01_PHI_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "San Diego Padres @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-22_CHC_COL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-07-25_CLE_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Kansas City Royals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-01_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-07-10_CHW_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Chicago White Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-08_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-10-10_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-03-22_CHW_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Chicago White Sox", - "severity": "medium" - }, - { - "game_key": "2025-03-23_MIL_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-10-01_CIN_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-03-24_MIN_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Minnesota Twins", - "severity": "medium" - }, - { - "game_key": "2025-03-22_BAL_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Pittsburgh Pirates", - "severity": "medium" - }, - { - "game_key": "2025-05-13_PHI_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-05-03_NYM_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "New York Mets @ St. Louis Cardinals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-17_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-03-22_ARI_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ San Francisco Giants", - "severity": "medium" - }, - { - "game_key": "2025-03-21_LAA_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Texas Rangers @ Los Angeles Angels", - "severity": "medium" - }, - { - "game_key": "2025-10-11_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-05-05_CLE_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-04-23_COL_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Colorado Rockies @ Kansas City Royals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-20_BAL_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Baltimore Orioles", - "severity": "medium" - }, - { - "game_key": "2025-10-01_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "medium" - }, - { - "game_key": "2025-10-12_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-20_HOU_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ Miami Marlins", - "severity": "medium" - }, - { - "game_key": "2025-10-07_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Detroit Tigers", - "severity": "medium" - }, - { - "game_key": "2025-03-21_MIN_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Minnesota Twins @ Philadelphia Phillies", - "severity": "medium" - }, - { - "game_key": "2025-03-21_NYM_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "St. Louis Cardinals @ New York Mets", - "severity": "medium" - }, - { - "game_key": "2025-10-16_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-03-21_KCR_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Kansas City Royals", - "severity": "medium" - }, - { - "game_key": "2025-03-23_COL_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-10-07_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-03-22_ATL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Detroit Tigers", - "severity": "medium" - }, - { - "game_key": "2025-04-25_BAL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Baltimore Orioles @ Detroit Tigers", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-22_NYM_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Washington Nationals @ New York Mets", - "severity": "medium" - }, - { - "game_key": "2025-03-21_ARI_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-05-28_ATL_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-10-08_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-03-24_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "medium" - }, - { - "game_key": "2025-04-29_CIN_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Cincinnati Reds", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-20_CHW_LAA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Chicago White Sox", - "severity": "medium" - }, - { - "game_key": "2025-07-09_BAL_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "New York Mets @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-04-06_ATL_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Miami Marlins @ Atlanta Braves", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-23_ATL_BAL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Atlanta Braves", - "severity": "medium" - }, - { - "game_key": "2025-05-20_CLE_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Minnesota Twins", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_DET_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ San Francisco Giants", - "severity": "medium" - }, - { - "game_key": "2025-10-08_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Detroit Tigers", - "severity": "medium" - }, - { - "game_key": "2025-03-21_BOS_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Boston Red Sox", - "severity": "medium" - }, - { - "game_key": "2025-10-17_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-06-18_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Milwaukee Brewers @ Chicago Cubs", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-09-30_CIN_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-23_CIN_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Cincinnati Reds", - "severity": "medium" - }, - { - "game_key": "2025-10-06_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "medium" - }, - { - "game_key": "2025-10-29_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-04-26_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Toronto Blue Jays @ New York Yankees", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-02_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-03-25_COL_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Minnesota Twins", - "severity": "medium" - }, - { - "game_key": "2025-03-24_ATL_CHC", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-03-20_NYM_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Mets @ Washington Nationals", - "severity": "medium" - }, - { - "game_key": "2025-03-25_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Los Angeles Angels", - "severity": "medium" - }, - { - "game_key": "2025-09-30_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-10-08_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ New York Yankees", - "severity": "medium" - }, - { - "game_key": "2025-05-06_COL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Detroit Tigers @ Colorado Rockies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-02_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-08-03_ATL_CIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Cincinnati Reds", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_CHW_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Chicago White Sox", - "severity": "medium" - }, - { - "game_key": "2025-03-20_CIN_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Cincinnati Reds", - "severity": "medium" - }, - { - "game_key": "2025-04-11_BAL_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Toronto Blue Jays @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-09_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-03-23_CHC_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Chicago Cubs", - "severity": "medium" - }, - { - "game_key": "2025-03-25_ARI_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Arizona D'Backs", - "severity": "medium" - }, - { - "game_key": "2025-03-21_CLE_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ San Francisco Giants", - "severity": "medium" - }, - { - "game_key": "2025-03-21_ATL_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Tampa Bay Rays", - "severity": "medium" - }, - { - "game_key": "2025-03-23_PIT_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-10-13_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-22_HOU_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ Houston Astros", - "severity": "medium" - }, - { - "game_key": "2025-03-22_CHW_COL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago White Sox @ Colorado Rockies", - "severity": "medium" - }, - { - "game_key": "2025-03-21_PHI_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-10-16_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-10-09_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-04-05_BOS_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Boston Red Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-25_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-03-25_DET_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ San Francisco Giants", - "severity": "medium" - }, - { - "game_key": "2025-03-21_BAL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Baltimore Orioles", - "severity": "medium" - }, - { - "game_key": "2025-04-25_BOS_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Boston Red Sox @ Cleveland Guardians", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-27_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "medium" - }, - { - "game_key": "2025-10-05_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-10-31_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-10-19_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-11-01_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "medium" - }, - { - "game_key": "2025-05-13_BAL_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Minnesota Twins @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_COL_LAA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Colorado Rockies", - "severity": "medium" - }, - { - "game_key": "2025-03-22_CLE_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Cleveland Guardians", - "severity": "medium" - }, - { - "game_key": "2025-03-24_PHI_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ Philadelphia Phillies", - "severity": "medium" - }, - { - "game_key": "2025-03-20_COL_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Texas Rangers", - "severity": "medium" - }, - { - "game_key": "2025-03-24_COL_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Colorado Rockies", - "severity": "medium" - }, - { - "game_key": "2025-10-15_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "medium" - }, - { - "game_key": "2025-03-21_MIA_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ Washington Nationals", - "severity": "medium" - }, - { - "game_key": "2025-05-21_ATL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - } - ], - "discrepancy_summary": { - "by_field": { - "missing": 155 - }, - "by_severity": { - "medium": 122, - "high": 33 - } - } - } - ], - "stadium_issues": [ - { - "stadium": "State Farm Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "TD Garden", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Barclays Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Spectrum Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "United Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Rocket Mortgage FieldHouse", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "American Airlines Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Ball Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Little Caesars Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Chase Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Toyota Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Gainbridge Fieldhouse", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Intuit Dome", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Crypto.com Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "FedExForum", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Kaseya Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Fiserv Forum", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Target Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Smoothie King Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Madison Square Garden", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Paycom Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Kia Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Wells Fargo Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Footprint Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Moda Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Golden 1 Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Frost Bank Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Scotiabank Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Delta Center", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - }, - { - "stadium": "Capital One Arena", - "sport": "NBA", - "issue": "Missing capacity", - "severity": "low" - } - ] -} \ No newline at end of file diff --git a/Scripts/data/validation_report.json b/Scripts/data/validation_report.json deleted file mode 100644 index dd8c489..0000000 --- a/Scripts/data/validation_report.json +++ /dev/null @@ -1,1425 +0,0 @@ -{ - "generated_at": "2026-01-06T10:26:45.655639", - "season": 2025, - "game_validations": [ - { - "sport": "MLB", - "season": "2025", - "sources": [ - "statsapi.mlb.com", - "baseball-reference" - ], - "total_games_source1": 2464, - "total_games_source2": 2556, - "games_matched": 2402, - "games_missing_source1": 126, - "games_missing_source2": 29, - "discrepancies": [ - { - "game_key": "2025-03-22_CHW_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Chicago White Sox", - "severity": "high" - }, - { - "game_key": "2025-03-22_MIN_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Minnesota Twins", - "severity": "high" - }, - { - "game_key": "2025-10-14_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-03-24_COL_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Colorado Rockies", - "severity": "high" - }, - { - "game_key": "2025-10-28_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-10-11_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-10-08_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-03-21_PHI_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-16_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-20_BOS_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ Minnesota Twins", - "severity": "high" - }, - { - "game_key": "2025-03-21_ARI_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-07-25_CLE_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Kansas City Royals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_MIA_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ Washington Nationals", - "severity": "high" - }, - { - "game_key": "2025-04-25_BAL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Baltimore Orioles @ Detroit Tigers", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-25_ARI_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Arizona D'Backs", - "severity": "high" - }, - { - "game_key": "2025-03-23_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-05-28_ATL_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_BOS_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Boston Red Sox", - "severity": "high" - }, - { - "game_key": "2025-03-25_ATL_CHC", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-03-21_KCR_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Athletics", - "severity": "high" - }, - { - "game_key": "2025-03-21_BAL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Baltimore Orioles", - "severity": "high" - }, - { - "game_key": "2025-03-20_ARI_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Arizona D'Backs", - "severity": "high" - }, - { - "game_key": "2025-05-03_NYM_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "New York Mets @ St. Louis Cardinals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-19_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-03-22_NYM_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Washington Nationals @ New York Mets", - "severity": "high" - }, - { - "game_key": "2025-08-03_ATL_CIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Cincinnati Reds", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-07-01_MIL_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Milwaukee Brewers @ New York Mets", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-20_CHW_LAA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Chicago White Sox", - "severity": "high" - }, - { - "game_key": "2025-03-23_PIT_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-08_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Detroit Tigers", - "severity": "high" - }, - { - "game_key": "2025-10-12_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-07_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Detroit Tigers", - "severity": "high" - }, - { - "game_key": "2025-03-20_NYM_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Mets @ Washington Nationals", - "severity": "high" - }, - { - "game_key": "2025-03-21_HOU_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ St. Louis Cardinals", - "severity": "high" - }, - { - "game_key": "2025-03-25_COL_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Minnesota Twins", - "severity": "high" - }, - { - "game_key": "2025-10-01_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-15_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-23_ARI_CHW", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ Chicago White Sox", - "severity": "high" - }, - { - "game_key": "2025-03-24_NYM_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ New York Mets", - "severity": "high" - }, - { - "game_key": "2025-10-01_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-03-21_CIN_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-10-13_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-05-22_BAL_BOS", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Baltimore Orioles @ Boston Red Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-16_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-20_ATL_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Atlanta Braves", - "severity": "high" - }, - { - "game_key": "2025-10-05_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-06_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Philadelphia Phillies", - "severity": "high" - }, - { - "game_key": "2025-03-22_CHW_COL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago White Sox @ Colorado Rockies", - "severity": "high" - }, - { - "game_key": "2025-04-06_ATL_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Miami Marlins @ Atlanta Braves", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-20_TBR_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-10_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-20_COL_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Texas Rangers", - "severity": "high" - }, - { - "game_key": "2025-09-30_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "high" - }, - { - "game_key": "2025-07-01_PHI_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "San Diego Padres @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-06-18_CHW_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Chicago White Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-06-18_DET_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Pittsburgh Pirates @ Detroit Tigers", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-04_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-03-21_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-09_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-20_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-10-06_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago Cubs @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-03-22_BOS_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ Tampa Bay Rays", - "severity": "high" - }, - { - "game_key": "2025-07-01_DET_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Detroit Tigers @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-04-25_BOS_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Boston Red Sox @ Cleveland Guardians", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-06-04_KCR_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Kansas City Royals @ St. Louis Cardinals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-17_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-24_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "high" - }, - { - "game_key": "2025-03-21_LAA_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Texas Rangers @ Los Angeles Angels", - "severity": "high" - }, - { - "game_key": "2025-10-04_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Philadelphia Phillies", - "severity": "high" - }, - { - "game_key": "2025-03-23_HOU_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ Washington Nationals", - "severity": "high" - }, - { - "game_key": "2025-04-05_BOS_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Boston Red Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_CHW_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Chicago White Sox", - "severity": "high" - }, - { - "game_key": "2025-03-23_CIN_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Cincinnati Reds", - "severity": "high" - }, - { - "game_key": "2025-03-22_ATL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Detroit Tigers", - "severity": "high" - }, - { - "game_key": "2025-03-22_BAL_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Pittsburgh Pirates", - "severity": "high" - }, - { - "game_key": "2025-03-25_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "high" - }, - { - "game_key": "2025-03-24_DET_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ San Francisco Giants", - "severity": "high" - }, - { - "game_key": "2025-03-24_BAL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Washington Nationals", - "severity": "high" - }, - { - "game_key": "2025-11-01_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-03-23_STL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Washington Nationals @ St. Louis Cardinals", - "severity": "high" - }, - { - "game_key": "2025-10-02_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-03-23_ATL_BAL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Baltimore Orioles @ Atlanta Braves", - "severity": "high" - }, - { - "game_key": "2025-04-23_COL_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Colorado Rockies @ Kansas City Royals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-05-05_CLE_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_SDP_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ San Diego Padres", - "severity": "high" - }, - { - "game_key": "2025-03-23_CHC_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Athletics @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-07_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-10-24_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-03-25_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Los Angeles Angels", - "severity": "high" - }, - { - "game_key": "2025-03-20_BAL_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Baltimore Orioles", - "severity": "high" - }, - { - "game_key": "2025-03-23_MIA_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ New York Mets", - "severity": "high" - }, - { - "game_key": "2025-07-10_CHW_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Chicago White Sox", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-05-20_CLE_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Cleveland Guardians @ Minnesota Twins", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_ATL_CHC", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-04_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-22_CIN_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ San Diego Padres", - "severity": "high" - }, - { - "game_key": "2025-03-22_CHC_COL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-03-22_NYY_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-04-26_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Toronto Blue Jays @ New York Yankees", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-27_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-21_CLE_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ San Francisco Giants", - "severity": "high" - }, - { - "game_key": "2025-03-21_MIN_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Minnesota Twins @ Philadelphia Phillies", - "severity": "high" - }, - { - "game_key": "2025-10-01_CIN_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-04-18_COL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Washington Nationals @ Colorado Rockies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-23_MIL_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-23_BOS_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Minnesota Twins @ Boston Red Sox", - "severity": "high" - }, - { - "game_key": "2025-03-22_HOU_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Miami Marlins @ Houston Astros", - "severity": "high" - }, - { - "game_key": "2025-03-25_DET_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ San Francisco Giants", - "severity": "high" - }, - { - "game_key": "2025-03-21_NYM_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "St. Louis Cardinals @ New York Mets", - "severity": "high" - }, - { - "game_key": "2025-03-20_CIN_KCR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Cincinnati Reds", - "severity": "high" - }, - { - "game_key": "2025-03-21_CHW_CIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Chicago White Sox @ Cincinnati Reds", - "severity": "high" - }, - { - "game_key": "2025-10-08_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-04-29_CIN_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Cincinnati Reds", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-02_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "high" - }, - { - "game_key": "2025-05-06_COL_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Detroit Tigers @ Colorado Rockies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-10-13_LAD_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-09-30_CIN_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cincinnati Reds @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-10-17_SEA_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-23_DET_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Philadelphia Phillies", - "severity": "high" - }, - { - "game_key": "2025-10-02_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-10-09_LAD_PHI", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Philadelphia Phillies @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-22_CLE_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Cleveland Guardians", - "severity": "high" - }, - { - "game_key": "2025-03-22_MIL_OAK", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Milwaukee Brewers @ Athletics", - "severity": "high" - }, - { - "game_key": "2025-03-24_ARI_CLE", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Cleveland Guardians @ Arizona D'Backs", - "severity": "high" - }, - { - "game_key": "2025-10-05_DET_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Seattle Mariners", - "severity": "high" - }, - { - "game_key": "2025-03-24_PHI_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ Philadelphia Phillies", - "severity": "high" - }, - { - "game_key": "2025-03-22_MIA_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "St. Louis Cardinals @ Miami Marlins", - "severity": "high" - }, - { - "game_key": "2025-07-09_BAL_NYM", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "New York Mets @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-21_COL_LAA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Angels @ Colorado Rockies", - "severity": "high" - }, - { - "game_key": "2025-03-22_ARI_SFG", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Arizona D'Backs @ San Francisco Giants", - "severity": "high" - }, - { - "game_key": "2025-03-21_KCR_SEA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Seattle Mariners @ Kansas City Royals", - "severity": "high" - }, - { - "game_key": "2025-03-21_DET_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Detroit Tigers", - "severity": "high" - }, - { - "game_key": "2025-10-01_CLE_DET", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Detroit Tigers @ Cleveland Guardians", - "severity": "high" - }, - { - "game_key": "2025-03-22_KCR_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Kansas City Royals @ Texas Rangers", - "severity": "high" - }, - { - "game_key": "2025-10-31_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-03-23_NYY_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Tampa Bay Rays @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-03-23_COL_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Colorado Rockies @ Milwaukee Brewers", - "severity": "high" - }, - { - "game_key": "2025-04-11_BAL_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Toronto Blue Jays @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-06-18_CHC_MIL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Milwaukee Brewers @ Chicago Cubs", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-25_MIA_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Miami Marlins", - "severity": "high" - }, - { - "game_key": "2025-05-13_BAL_MIN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Minnesota Twins @ Baltimore Orioles", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_LAA_LAD", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Los Angeles Angels", - "severity": "high" - }, - { - "game_key": "2025-03-21_ATL_TBR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Atlanta Braves @ Tampa Bay Rays", - "severity": "high" - }, - { - "game_key": "2025-10-08_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-05-21_ATL_WSN", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "Atlanta Braves @ Washington Nationals", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-05-13_PHI_STL", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "St. Louis Cardinals @ Philadelphia Phillies", - "value2": "NOT FOUND", - "severity": "high" - }, - { - "game_key": "2025-03-24_MIN_PIT", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Pittsburgh Pirates @ Minnesota Twins", - "severity": "high" - }, - { - "game_key": "2025-03-20_SDP_TEX", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Texas Rangers @ San Diego Padres", - "severity": "high" - }, - { - "game_key": "2025-09-30_BOS_NYY", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Boston Red Sox @ New York Yankees", - "severity": "high" - }, - { - "game_key": "2025-10-25_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Los Angeles Dodgers @ Toronto Blue Jays", - "severity": "high" - }, - { - "game_key": "2025-09-30_CHC_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Chicago Cubs", - "severity": "high" - }, - { - "game_key": "2025-03-20_HOU_MIA", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Houston Astros @ Miami Marlins", - "severity": "high" - }, - { - "game_key": "2025-10-29_LAD_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "Toronto Blue Jays @ Los Angeles Dodgers", - "severity": "high" - }, - { - "game_key": "2025-03-23_ARI_SDP", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "San Diego Padres @ Arizona D'Backs", - "severity": "high" - }, - { - "game_key": "2025-10-04_NYY_TOR", - "field": "missing", - "source1": "statsapi.mlb.com", - "source2": "baseball-reference", - "value1": "NOT FOUND", - "value2": "New York Yankees @ Toronto Blue Jays", - "severity": "high" - } - ], - "discrepancy_summary": { - "by_field": { - "missing": 155 - }, - "by_severity": { - "high": 155 - } - } - } - ], - "stadium_issues": [] -} \ No newline at end of file diff --git a/SportsTime/Core/Models/Domain/Region.swift b/SportsTime/Core/Models/Domain/Region.swift index 5664d6d..da34f86 100644 --- a/SportsTime/Core/Models/Domain/Region.swift +++ b/SportsTime/Core/Models/Domain/Region.swift @@ -7,7 +7,7 @@ import Foundation -enum Region: String, CaseIterable, Identifiable { +enum Region: String, CaseIterable, Identifiable, Codable, Hashable { case east = "East Coast" case central = "Central" case west = "West Coast" diff --git a/SportsTime/Core/Models/Domain/TripPreferences.swift b/SportsTime/Core/Models/Domain/TripPreferences.swift index c33cdab..5c23a76 100644 --- a/SportsTime/Core/Models/Domain/TripPreferences.swift +++ b/SportsTime/Core/Models/Domain/TripPreferences.swift @@ -227,7 +227,8 @@ struct TripPreferences: Codable, Hashable { var lodgingType: LodgingType var numberOfDrivers: Int var maxDrivingHoursPerDriver: Double? - var maxTripOptions: Int + var allowRepeatCities: Bool + var selectedRegions: Set init( planningMode: PlanningMode = .dateRange, @@ -248,7 +249,8 @@ struct TripPreferences: Codable, Hashable { lodgingType: LodgingType = .hotel, numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double? = nil, - maxTripOptions: Int = 10 + allowRepeatCities: Bool = true, + selectedRegions: Set = [.east, .central, .west] ) { self.planningMode = planningMode self.startLocation = startLocation @@ -268,7 +270,8 @@ struct TripPreferences: Codable, Hashable { self.lodgingType = lodgingType self.numberOfDrivers = numberOfDrivers self.maxDrivingHoursPerDriver = maxDrivingHoursPerDriver - self.maxTripOptions = maxTripOptions + self.allowRepeatCities = allowRepeatCities + self.selectedRegions = selectedRegions } var totalDriverHoursPerDay: Double { diff --git a/SportsTime/Core/Services/DataProvider.swift b/SportsTime/Core/Services/DataProvider.swift index 2c1c159..c1e3efa 100644 --- a/SportsTime/Core/Services/DataProvider.swift +++ b/SportsTime/Core/Services/DataProvider.swift @@ -156,7 +156,7 @@ final class AppDataProvider: ObservableObject { let canonicalGames = try context.fetch(descriptor) // Filter by sport and convert to domain models - return canonicalGames.compactMap { canonical -> Game? in + let result = canonicalGames.compactMap { canonical -> Game? in guard sportStrings.contains(canonical.sport) else { return nil } let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID() @@ -169,6 +169,8 @@ final class AppDataProvider: ObservableObject { stadiumUUID: stadiumUUID ) } + + return result } /// Fetch a single game by ID diff --git a/SportsTime/Core/Services/SuggestedTripsGenerator.swift b/SportsTime/Core/Services/SuggestedTripsGenerator.swift index 703946f..8844b15 100644 --- a/SportsTime/Core/Services/SuggestedTripsGenerator.swift +++ b/SportsTime/Core/Services/SuggestedTripsGenerator.swift @@ -243,8 +243,7 @@ final class SuggestedTripsGenerator { sports: sports, startDate: tripStartDate, endDate: tripEndDate, - leisureLevel: .moderate, - maxTripOptions: 1 + leisureLevel: .moderate ) let request = PlanningRequest( @@ -421,8 +420,7 @@ final class SuggestedTripsGenerator { sports: sports, startDate: tripStartDate, endDate: tripEndDate, - leisureLevel: .moderate, - maxTripOptions: 1 + leisureLevel: .moderate ) // Generate travel segments between stops diff --git a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift index e1f9816..9a23e28 100644 --- a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift @@ -26,10 +26,6 @@ final class SettingsViewModel { didSet { savePreferences() } } - var maxTripOptions: Int { - didSet { savePreferences() } - } - // MARK: - Sync State private(set) var isSyncing = false @@ -61,9 +57,6 @@ final class SettingsViewModel { let savedDrivingHours = defaults.integer(forKey: "maxDrivingHoursPerDay") self.maxDrivingHoursPerDay = savedDrivingHours == 0 ? 8 : savedDrivingHours - let savedMaxTripOptions = defaults.integer(forKey: "maxTripOptions") - self.maxTripOptions = savedMaxTripOptions == 0 ? 10 : savedMaxTripOptions - // Last sync self.lastSyncDate = defaults.object(forKey: "lastSyncDate") as? Date @@ -101,7 +94,6 @@ final class SettingsViewModel { selectedTheme = .teal selectedSports = Set(Sport.supported) maxDrivingHoursPerDay = 8 - maxTripOptions = 10 } // MARK: - Persistence @@ -110,6 +102,5 @@ final class SettingsViewModel { let defaults = UserDefaults.standard defaults.set(selectedSports.map(\.rawValue), forKey: "selectedSports") defaults.set(maxDrivingHoursPerDay, forKey: "maxDrivingHoursPerDay") - defaults.set(maxTripOptions, forKey: "maxTripOptions") } } diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 15ad177..97c1b73 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -141,22 +141,6 @@ struct SettingsView: View { ) } - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Trip Options to Show") - Spacer() - Text("\(viewModel.maxTripOptions)") - .foregroundStyle(.secondary) - } - Slider( - value: Binding( - get: { Double(viewModel.maxTripOptions) }, - set: { viewModel.maxTripOptions = Int($0) } - ), - in: 1...20, - step: 1 - ) - } } header: { Text("Travel Preferences") } footer: { diff --git a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift index 25043ca..f8693e4 100644 --- a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift +++ b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift @@ -86,6 +86,10 @@ final class TripCreationViewModel { var numberOfDrivers: Int = 1 var maxDrivingHoursPerDriver: Double = 8 + // Travel Preferences + var allowRepeatCities: Bool = true + var selectedRegions: Set = [.east, .central, .west] + // MARK: - Dependencies private let planningEngine = TripPlanningEngine() @@ -273,10 +277,6 @@ final class TripCreationViewModel { await loadScheduleData() } - // Read max trip options from settings (default 10) - let savedMaxOptions = UserDefaults.standard.integer(forKey: "maxTripOptions") - let maxTripOptions = savedMaxOptions > 0 ? min(20, savedMaxOptions) : 10 - // Build preferences let preferences = TripPreferences( planningMode: planningMode, @@ -297,7 +297,8 @@ final class TripCreationViewModel { lodgingType: lodgingType, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, - maxTripOptions: maxTripOptions + allowRepeatCities: allowRepeatCities, + selectedRegions: selectedRegions ) // Build planning request @@ -451,6 +452,17 @@ final class TripCreationViewModel { availableGames = [] isLoadingGames = false currentPreferences = nil + allowRepeatCities = true + selectedRegions = [.east, .central, .west] + } + + /// Toggles region selection. Any combination is allowed. + func toggleRegion(_ region: Region) { + if selectedRegions.contains(region) { + selectedRegions.remove(region) + } else { + selectedRegions.insert(region) + } } /// Select a specific itinerary option and navigate to its detail @@ -465,9 +477,6 @@ final class TripCreationViewModel { /// Convert an itinerary option to a Trip (public for use by TripOptionsView) func convertOptionToTrip(_ option: ItineraryOption) -> Trip { - let savedMaxOptions = UserDefaults.standard.integer(forKey: "maxTripOptions") - let maxOptions = savedMaxOptions > 0 ? min(20, savedMaxOptions) : 10 - let preferences = currentPreferences ?? TripPreferences( planningMode: planningMode, startLocation: nil, @@ -487,7 +496,8 @@ final class TripCreationViewModel { lodgingType: lodgingType, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, - maxTripOptions: maxOptions + allowRepeatCities: allowRepeatCities, + selectedRegions: selectedRegions ) return convertToTrip(option: option, preferences: preferences) } diff --git a/SportsTime/Features/Trip/Views/RegionMapSelector.swift b/SportsTime/Features/Trip/Views/RegionMapSelector.swift new file mode 100644 index 0000000..2f359b9 --- /dev/null +++ b/SportsTime/Features/Trip/Views/RegionMapSelector.swift @@ -0,0 +1,213 @@ +// +// RegionMapSelector.swift +// SportsTime +// +// Interactive map for selecting travel regions. +// + +import SwiftUI + +/// A map-based selector for choosing geographic regions. +/// Shows North America with three selectable zones: West, Central, East. +/// +/// Selection rules: +/// - Can select: East, Central, West, East+Central, Central+West +/// - Cannot select: East+West (must have Central between them) +struct RegionMapSelector: View { + @Binding var selectedRegions: Set + let onToggle: (Region) -> Void + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(spacing: Theme.Spacing.sm) { + // Map with regions + GeometryReader { geometry in + ZStack { + // Background map outline + mapBackground + + // Selectable regions + HStack(spacing: 0) { + regionButton(.west) + regionButton(.central) + regionButton(.east) + } + } + } + .frame(height: 140) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke(Theme.textMuted(colorScheme).opacity(0.5), lineWidth: 1) + ) + + // Legend + selectionLegend + } + } + + // MARK: - Map Background + + private var mapBackground: some View { + ZStack { + // Simple gradient background representing land + LinearGradient( + colors: [ + Color.green.opacity(0.15), + Color.green.opacity(0.1), + Color.green.opacity(0.15) + ], + startPoint: .leading, + endPoint: .trailing + ) + + // Subtle grid lines for visual separation + HStack(spacing: 0) { + Color.clear + .frame(maxWidth: .infinity) + Rectangle() + .fill(Theme.textMuted(colorScheme).opacity(0.3)) + .frame(width: 1) + Color.clear + .frame(maxWidth: .infinity) + Rectangle() + .fill(Theme.textMuted(colorScheme).opacity(0.3)) + .frame(width: 1) + Color.clear + .frame(maxWidth: .infinity) + } + } + } + + // MARK: - Region Button + + private func regionButton(_ region: Region) -> some View { + let isSelected = selectedRegions.contains(region) + let isDisabled = isRegionDisabled(region) + + return Button { + onToggle(region) + } label: { + VStack(spacing: Theme.Spacing.xs) { + // Region icon + Image(systemName: iconForRegion(region)) + .font(.system(size: 24)) + .foregroundStyle(isSelected ? .white : Theme.textSecondary(colorScheme)) + + // Region name + Text(region.shortName) + .font(.system(size: Theme.FontSize.caption, weight: .semibold)) + .foregroundStyle(isSelected ? .white : Theme.textPrimary(colorScheme)) + + // Cities hint + Text(citiesForRegion(region)) + .font(.system(size: Theme.FontSize.micro)) + .foregroundStyle(isSelected ? .white.opacity(0.8) : Theme.textMuted(colorScheme)) + .lineLimit(2) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + isSelected + ? regionColor(region) + : Color.clear + ) + .opacity(isDisabled ? 0.4 : 1.0) + } + .buttonStyle(.plain) + .disabled(isDisabled) + } + + // MARK: - Helpers + + private func iconForRegion(_ region: Region) -> String { + switch region { + case .west: return "sun.max.fill" + case .central: return "building.2.fill" + case .east: return "building.columns.fill" + case .crossCountry: return "arrow.left.arrow.right" + } + } + + private func citiesForRegion(_ region: Region) -> String { + switch region { + case .west: return "LA, SF, Seattle" + case .central: return "Chicago, Houston, Denver" + case .east: return "NYC, Boston, Miami" + case .crossCountry: return "" + } + } + + private func regionColor(_ region: Region) -> Color { + switch region { + case .west: return .orange + case .central: return .blue + case .east: return .green + case .crossCountry: return .purple + } + } + + /// East and West cannot both be selected (not adjacent) + private func isRegionDisabled(_ region: Region) -> Bool { + // If trying to show East+West as disabled, we handle that in toggle logic instead + // This is for visual indication only + return false + } + + // MARK: - Legend + + private var selectionLegend: some View { + HStack(spacing: Theme.Spacing.md) { + if selectedRegions.isEmpty { + Text("Tap regions to select") + .font(.system(size: Theme.FontSize.micro)) + .foregroundStyle(Theme.textMuted(colorScheme)) + } else { + Text("Selected: \(selectedRegions.map { $0.shortName }.sorted().joined(separator: " + "))") + .font(.system(size: Theme.FontSize.caption, weight: .medium)) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + + Button { + selectedRegions.removeAll() + } label: { + Text("Clear") + .font(.system(size: Theme.FontSize.micro)) + .foregroundStyle(Theme.warmOrange) + } + } + } + } +} + +#Preview { + struct PreviewWrapper: View { + @State private var selected: Set = [.central] + + var body: some View { + VStack(spacing: 20) { + RegionMapSelector(selectedRegions: $selected) { region in + if selected.contains(region) { + selected.remove(region) + } else { + // Adjacency rule + if region == .east { + selected.remove(.west) + } else if region == .west { + selected.remove(.east) + } + selected.insert(region) + } + } + .padding() + + Text("Selected: \(selected.map { $0.shortName }.joined(separator: ", "))") + } + .padding() + } + } + + return PreviewWrapper() +} diff --git a/SportsTime/Features/Trip/Views/TripCreationView.swift b/SportsTime/Features/Trip/Views/TripCreationView.swift index 40c78d8..6ad9567 100644 --- a/SportsTime/Features/Trip/Views/TripCreationView.swift +++ b/SportsTime/Features/Trip/Views/TripCreationView.swift @@ -78,7 +78,6 @@ struct TripCreationView: View { // Common sections travelSection - constraintsSection optionalSection // Validation message @@ -591,6 +590,32 @@ struct TripCreationView: View { private var travelSection: some View { ThemedSection(title: "Travel") { VStack(spacing: Theme.Spacing.md) { + // Region selector + VStack(alignment: .leading, spacing: Theme.Spacing.xs) { + Text("Regions") + .font(.system(size: Theme.FontSize.caption)) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + RegionMapSelector( + selectedRegions: $viewModel.selectedRegions, + onToggle: { region in + viewModel.toggleRegion(region) + } + ) + + if viewModel.selectedRegions.isEmpty { + Text("Select at least one region") + .font(.system(size: Theme.FontSize.micro)) + .foregroundStyle(Theme.warmOrange) + .padding(.top, Theme.Spacing.xxs) + } else { + Text("Games will be found in selected regions") + .font(.system(size: Theme.FontSize.micro)) + .foregroundStyle(Theme.textMuted(colorScheme)) + .padding(.top, Theme.Spacing.xxs) + } + } + // Route preference VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Route Preference") @@ -604,53 +629,22 @@ struct TripCreationView: View { } .pickerStyle(.segmented) } - } - } - } - private var constraintsSection: some View { - ThemedSection(title: "Trip Style") { - VStack(spacing: Theme.Spacing.md) { + // Allow repeat cities ThemedToggle( - label: "Limit Cities", - isOn: $viewModel.useStopCount, - icon: "mappin.and.ellipse" + label: "Allow Repeat Cities", + isOn: $viewModel.allowRepeatCities, + icon: "arrow.triangle.2.circlepath" ) - if viewModel.useStopCount { - VStack(alignment: .leading, spacing: Theme.Spacing.xs) { - ThemedStepper( - label: "Number of Cities", - value: viewModel.numberOfStops, - range: 1...20, - onIncrement: { viewModel.numberOfStops += 1 }, - onDecrement: { viewModel.numberOfStops -= 1 } - ) - - Text("How many different cities to visit on your trip. More cities = more variety, but more driving between them.") - .font(.system(size: Theme.FontSize.micro)) - .foregroundStyle(Theme.textMuted(colorScheme)) - } - } - - VStack(alignment: .leading, spacing: Theme.Spacing.xs) { - Text("Trip Pace") - .font(.system(size: Theme.FontSize.caption)) - .foregroundStyle(Theme.textSecondary(colorScheme)) - - Picker("Pace", selection: $viewModel.leisureLevel) { - ForEach(LeisureLevel.allCases) { level in - Text(level.displayName).tag(level) - } - } - .pickerStyle(.segmented) - - Text(viewModel.leisureLevel.description) + if !viewModel.allowRepeatCities { + Text("Each city will only be visited on one day") .font(.system(size: Theme.FontSize.micro)) .foregroundStyle(Theme.textMuted(colorScheme)) - .padding(.top, Theme.Spacing.xxs) + .padding(.leading, 32) } } + .animation(.easeInOut(duration: 0.2), value: viewModel.selectedRegions) } } @@ -1220,6 +1214,48 @@ enum TripSortOption: String, CaseIterable, Identifiable { } } +enum TripPaceFilter: String, CaseIterable, Identifiable { + case all = "All" + case packed = "Packed" + case moderate = "Moderate" + case relaxed = "Relaxed" + + var id: String { rawValue } + + var icon: String { + switch self { + case .all: return "rectangle.stack" + case .packed: return "flame" + case .moderate: return "equal.circle" + case .relaxed: return "leaf" + } + } +} + +enum CitiesFilter: Int, CaseIterable, Identifiable { + case noLimit = 100 + case fifteen = 15 + case ten = 10 + case five = 5 + case four = 4 + case three = 3 + case two = 2 + + var id: Int { rawValue } + + var displayName: String { + switch self { + case .noLimit: return "No Limit" + case .fifteen: return "15" + case .ten: return "10" + case .five: return "5" + case .four: return "4" + case .three: return "3" + case .two: return "2" + } + } +} + struct TripOptionsView: View { let options: [ItineraryOption] let games: [UUID: RichGame] @@ -1229,23 +1265,55 @@ struct TripOptionsView: View { @State private var selectedTrip: Trip? @State private var showTripDetail = false @State private var sortOption: TripSortOption = .recommended + @State private var citiesFilter: CitiesFilter = .noLimit + @State private var paceFilter: TripPaceFilter = .all @Environment(\.colorScheme) private var colorScheme - private var sortedOptions: [ItineraryOption] { + // MARK: - Computed Properties + + private func uniqueCityCount(for option: ItineraryOption) -> Int { + Set(option.stops.map { $0.city }).count + } + + private var filteredAndSortedOptions: [ItineraryOption] { + // Apply filters first + let filtered = options.filter { option in + let cityCount = uniqueCityCount(for: option) + + // City filter + guard cityCount <= citiesFilter.rawValue else { return false } + + // Pace filter based on games per day ratio + switch paceFilter { + case .all: + return true + case .packed: + // High game density: > 0.8 games per day + return gamesPerDay(for: option) >= 0.8 + case .moderate: + // Medium density: 0.4-0.8 games per day + let gpd = gamesPerDay(for: option) + return gpd >= 0.4 && gpd < 0.8 + case .relaxed: + // Low density: < 0.4 games per day + return gamesPerDay(for: option) < 0.4 + } + } + + // Then apply sorting switch sortOption { case .recommended: - return options + return filtered case .mostGames: - return options.sorted { $0.totalGames > $1.totalGames } + return filtered.sorted { $0.totalGames > $1.totalGames } case .leastGames: - return options.sorted { $0.totalGames < $1.totalGames } + return filtered.sorted { $0.totalGames < $1.totalGames } case .mostMiles: - return options.sorted { $0.totalDistanceMiles > $1.totalDistanceMiles } + return filtered.sorted { $0.totalDistanceMiles > $1.totalDistanceMiles } case .leastMiles: - return options.sorted { $0.totalDistanceMiles < $1.totalDistanceMiles } + return filtered.sorted { $0.totalDistanceMiles < $1.totalDistanceMiles } case .bestEfficiency: - // Games per driving hour (higher is better) - return options.sorted { + return filtered.sorted { let effA = $0.totalDrivingHours > 0 ? Double($0.totalGames) / $0.totalDrivingHours : 0 let effB = $1.totalDrivingHours > 0 ? Double($1.totalGames) / $1.totalDrivingHours : 0 return effA > effB @@ -1253,42 +1321,48 @@ struct TripOptionsView: View { } } + private func gamesPerDay(for option: ItineraryOption) -> Double { + guard let first = option.stops.first, + let last = option.stops.last else { return 0 } + let days = max(1, Calendar.current.dateComponents([.day], from: first.arrivalDate, to: last.departureDate).day ?? 1) + return Double(option.totalGames) / Double(days) + } + var body: some View { ScrollView { - LazyVStack(spacing: 20) { + LazyVStack(spacing: 16) { // Hero header - VStack(spacing: 12) { + VStack(spacing: 8) { Image(systemName: "point.topright.arrow.triangle.backward.to.point.bottomleft.scurvepath.fill") - .font(.system(size: 44)) + .font(.system(size: 40)) .foregroundStyle(Theme.warmOrange) - Text("\(options.count) Routes Found") + Text("\(filteredAndSortedOptions.count) of \(options.count) Routes") .font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded)) .foregroundStyle(Theme.textPrimary(colorScheme)) - - Text("Each route offers a unique adventure") - .font(.system(size: Theme.FontSize.body)) - .foregroundStyle(Theme.textSecondary(colorScheme)) } - .padding(.top, Theme.Spacing.xl) - .padding(.bottom, Theme.Spacing.sm) + .padding(.top, Theme.Spacing.lg) - // Sort picker - sortPicker + // Filters section + filtersSection .padding(.horizontal, Theme.Spacing.md) - .padding(.bottom, Theme.Spacing.sm) // Options list - ForEach(sortedOptions) { option in - TripOptionCard( - option: option, - games: games, - onSelect: { - selectedTrip = convertToTrip(option) - showTripDetail = true - } - ) - .padding(.horizontal, Theme.Spacing.md) + if filteredAndSortedOptions.isEmpty { + emptyFilterState + .padding(.top, Theme.Spacing.xl) + } else { + ForEach(filteredAndSortedOptions) { option in + TripOptionCard( + option: option, + games: games, + onSelect: { + selectedTrip = convertToTrip(option) + showTripDetail = true + } + ) + .padding(.horizontal, Theme.Spacing.md) + } } } .padding(.bottom, Theme.Spacing.xxl) @@ -1337,6 +1411,115 @@ struct TripOptionsView: View { ) } } + + // MARK: - Filters Section + + private var filtersSection: some View { + VStack(spacing: Theme.Spacing.md) { + // Sort and Pace row + HStack(spacing: Theme.Spacing.sm) { + sortPicker + Spacer() + pacePicker + } + + // Cities picker + citiesPicker + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + + private var pacePicker: some View { + Menu { + ForEach(TripPaceFilter.allCases) { pace in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + paceFilter = pace + } + } label: { + Label(pace.rawValue, systemImage: pace.icon) + } + } + } label: { + HStack(spacing: 6) { + Image(systemName: paceFilter.icon) + .font(.system(size: 12)) + Text(paceFilter.rawValue) + .font(.system(size: 13, weight: .medium)) + Image(systemName: "chevron.down") + .font(.system(size: 10)) + } + .foregroundStyle(paceFilter == .all ? Theme.textPrimary(colorScheme) : Theme.warmOrange) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(paceFilter == .all ? Theme.cardBackground(colorScheme) : Theme.warmOrange.opacity(0.15)) + .clipShape(Capsule()) + .overlay( + Capsule() + .strokeBorder(paceFilter == .all ? Theme.textMuted(colorScheme).opacity(0.2) : Theme.warmOrange.opacity(0.3), lineWidth: 1) + ) + } + } + + private var citiesPicker: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.xs) { + Label("Max Cities", systemImage: "mappin.circle") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(CitiesFilter.allCases) { filter in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + citiesFilter = filter + } + } label: { + Text(filter.displayName) + .font(.system(size: 13, weight: citiesFilter == filter ? .semibold : .medium)) + .foregroundStyle(citiesFilter == filter ? .white : Theme.textPrimary(colorScheme)) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(citiesFilter == filter ? Theme.warmOrange : Theme.cardBackground(colorScheme)) + .clipShape(Capsule()) + .overlay( + Capsule() + .strokeBorder(citiesFilter == filter ? Color.clear : Theme.textMuted(colorScheme).opacity(0.2), lineWidth: 1) + ) + } + .buttonStyle(.plain) + } + } + } + } + } + + private var emptyFilterState: some View { + VStack(spacing: Theme.Spacing.md) { + Image(systemName: "line.3.horizontal.decrease.circle") + .font(.system(size: 48)) + .foregroundStyle(Theme.textMuted(colorScheme)) + + Text("No routes match your filters") + .font(.system(size: Theme.FontSize.body, weight: .medium)) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + Button { + withAnimation { + citiesFilter = .noLimit + paceFilter = .all + } + } label: { + Text("Reset Filters") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(Theme.warmOrange) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, Theme.Spacing.xxl) + } } // MARK: - Trip Option Card diff --git a/SportsTime/Planning/Engine/GameDAGRouter.swift b/SportsTime/Planning/Engine/GameDAGRouter.swift index 4af8b81..e85d649 100644 --- a/SportsTime/Planning/Engine/GameDAGRouter.swift +++ b/SportsTime/Planning/Engine/GameDAGRouter.swift @@ -25,10 +25,11 @@ enum GameDAGRouter { // MARK: - Configuration /// Default beam width - how many partial routes to keep at each step - private static let defaultBeamWidth = 30 + /// Increased to ensure we preserve diverse route lengths (short and long trips) + private static let defaultBeamWidth = 50 - /// Maximum options to return - private static let maxOptions = 10 + /// Maximum options to return (increased to provide more diverse trip lengths) + private static let maxOptions = 50 /// Buffer time after game ends before we can depart (hours) private static let gameEndBufferHours: Double = 3.0 @@ -47,6 +48,7 @@ enum GameDAGRouter { /// - stadiums: Dictionary mapping stadium IDs to Stadium objects /// - constraints: Driving constraints (number of drivers, max hours per day) /// - anchorGameIds: Games that MUST appear in every valid route (for Scenario B) + /// - allowRepeatCities: If false, each city can only appear once in a route /// - beamWidth: How many partial routes to keep at each depth (default 30) /// /// - Returns: Array of valid game combinations, sorted by score (most games, least driving) @@ -56,6 +58,7 @@ enum GameDAGRouter { stadiums: [UUID: Stadium], constraints: DrivingConstraints, anchorGameIds: Set = [], + allowRepeatCities: Bool = true, beamWidth: Int = defaultBeamWidth ) -> [[Game]] { @@ -130,6 +133,15 @@ enum GameDAGRouter { // Try adding each of today's games for candidate in todaysGames { + // Check for repeat city violation during route building + if !allowRepeatCities { + let candidateCity = stadiums[candidate.stadiumId]?.city ?? "" + let pathCities = Set(path.compactMap { stadiums[$0.stadiumId]?.city }) + if pathCities.contains(candidateCity) { + continue // Skip - would violate allowRepeatCities + } + } + if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) { let newPath = path + [candidate] nextBeam.append(newPath) @@ -169,6 +181,7 @@ enum GameDAGRouter { from games: [Game], stadiums: [UUID: Stadium], anchorGameIds: Set = [], + allowRepeatCities: Bool = true, stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop] ) -> [[Game]] { // Use default driving constraints @@ -178,7 +191,8 @@ enum GameDAGRouter { games: games, stadiums: stadiums, constraints: constraints, - anchorGameIds: anchorGameIds + anchorGameIds: anchorGameIds, + allowRepeatCities: allowRepeatCities ) } @@ -288,8 +302,9 @@ enum GameDAGRouter { // MARK: - Geographic Diversity - /// Selects geographically diverse routes from the candidate set. - /// Groups routes by their primary city (where most games are) and picks the best from each region. + /// Selects diverse routes from the candidate set. + /// Ensures diversity by BOTH route length (city count) AND primary city. + /// This guarantees users see 2-city trips alongside 5+ city trips. private static func selectDiverseRoutes( _ routes: [[Game]], stadiums: [UUID: Stadium], @@ -297,58 +312,88 @@ enum GameDAGRouter { ) -> [[Game]] { guard !routes.isEmpty else { return [] } - // Group routes by primary city (the city with the most games in the route) - var routesByRegion: [String: [[Game]]] = [:] - + // Group routes by city count (route length) + var routesByLength: [Int: [[Game]]] = [:] for route in routes { - let primaryCity = getPrimaryCity(for: route, stadiums: stadiums) - routesByRegion[primaryCity, default: []].append(route) + let cityCount = Set(route.compactMap { stadiums[$0.stadiumId]?.city }).count + routesByLength[cityCount, default: []].append(route) } - // Sort routes within each region by score (best first) - for (region, regionRoutes) in routesByRegion { - routesByRegion[region] = regionRoutes.sorted { + // Sort routes within each length by score + for (length, lengthRoutes) in routesByLength { + routesByLength[length] = lengthRoutes.sorted { scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) } } - // Sort regions by their best route's score (so best regions come first) - let sortedRegions = routesByRegion.keys.sorted { region1, region2 in - let score1 = routesByRegion[region1]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0 - let score2 = routesByRegion[region2]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0 - return score1 > score2 - } + // Allocate slots to each length category + // Goal: ensure at least 1 route per length category if available + let sortedLengths = routesByLength.keys.sorted() + let minPerLength = max(1, maxCount / max(1, sortedLengths.count)) - - // Pick routes round-robin from each region to ensure diversity var selectedRoutes: [[Game]] = [] - var regionIndices: [String: Int] = [:] + var selectedIds = Set() - // First pass: get best route from each region - for region in sortedRegions { + // First pass: take best route(s) from each length category + for length in sortedLengths { if selectedRoutes.count >= maxCount { break } - if let regionRoutes = routesByRegion[region], !regionRoutes.isEmpty { - selectedRoutes.append(regionRoutes[0]) - regionIndices[region] = 1 - } - } - - // Second pass: fill remaining slots with next-best routes from top regions - var round = 1 - while selectedRoutes.count < maxCount { - var addedAny = false - for region in sortedRegions { - if selectedRoutes.count >= maxCount { break } - let idx = regionIndices[region] ?? 0 - if let regionRoutes = routesByRegion[region], idx < regionRoutes.count { - selectedRoutes.append(regionRoutes[idx]) - regionIndices[region] = idx + 1 - addedAny = true + if let lengthRoutes = routesByLength[length] { + let toTake = min(minPerLength, lengthRoutes.count, maxCount - selectedRoutes.count) + for route in lengthRoutes.prefix(toTake) { + let key = route.map { $0.id.uuidString }.joined(separator: "-") + if !selectedIds.contains(key) { + selectedRoutes.append(route) + selectedIds.insert(key) + } } } - if !addedAny { break } - round += 1 - if round > 5 { break } // Safety limit + } + + // Second pass: fill remaining slots, prioritizing geographic diversity + if selectedRoutes.count < maxCount { + // Group remaining routes by primary city + var remainingByCity: [String: [[Game]]] = [:] + for route in routes { + let key = route.map { $0.id.uuidString }.joined(separator: "-") + if !selectedIds.contains(key) { + let city = getPrimaryCity(for: route, stadiums: stadiums) + remainingByCity[city, default: []].append(route) + } + } + + // Sort by score within each city + for (city, cityRoutes) in remainingByCity { + remainingByCity[city] = cityRoutes.sorted { + scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) + } + } + + // Round-robin from each city + let sortedCities = remainingByCity.keys.sorted { city1, city2 in + let score1 = remainingByCity[city1]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0 + let score2 = remainingByCity[city2]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0 + return score1 > score2 + } + + var cityIndices: [String: Int] = [:] + while selectedRoutes.count < maxCount { + var addedAny = false + for city in sortedCities { + if selectedRoutes.count >= maxCount { break } + let idx = cityIndices[city] ?? 0 + if let cityRoutes = remainingByCity[city], idx < cityRoutes.count { + let route = cityRoutes[idx] + let key = route.map { $0.id.uuidString }.joined(separator: "-") + if !selectedIds.contains(key) { + selectedRoutes.append(route) + selectedIds.insert(key) + addedAny = true + } + cityIndices[city] = idx + 1 + } + } + if !addedAny { break } + } } return selectedRoutes @@ -412,6 +457,7 @@ enum GameDAGRouter { } /// Prunes dominated paths and truncates to beam width. + /// Maintains diversity by both ending city AND route length to ensure short trips aren't eliminated. private static func pruneAndTruncate( _ paths: [[Game]], beamWidth: Int, @@ -429,32 +475,47 @@ enum GameDAGRouter { } } - // Sort by score (best first) - let sorted = uniquePaths.sorted { scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) } + // Group paths by unique city count (route length) + // This ensures we keep short trips (2 cities) alongside long trips (5+ cities) + var pathsByLength: [Int: [[Game]]] = [:] + for path in uniquePaths { + let cityCount = Set(path.compactMap { stadiums[$0.stadiumId]?.city }).count + pathsByLength[cityCount, default: []].append(path) + } + + // Sort paths within each length group by score + for (length, lengthPaths) in pathsByLength { + pathsByLength[length] = lengthPaths.sorted { + scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) + } + } + + // Allocate beam slots proportionally to length groups, with minimum per group + let sortedLengths = pathsByLength.keys.sorted() + let minPerLength = max(2, beamWidth / max(1, sortedLengths.count)) - // Dominance pruning: within same ending city, keep only best paths var pruned: [[Game]] = [] - var bestByEndCity: [String: Double] = [:] - for path in sorted { - guard let lastGame = path.last else { continue } - let endCity = stadiums[lastGame.stadiumId]?.city ?? "Unknown" - let score = scorePath(path, stadiums: stadiums) - - // Keep if this is the best path ending in this city, or if score is within 20% of best - if let bestScore = bestByEndCity[endCity] { - if score >= bestScore * 0.8 { - pruned.append(path) - } - } else { - bestByEndCity[endCity] = score - pruned.append(path) + // First pass: take minimum from each length group + for length in sortedLengths { + if let lengthPaths = pathsByLength[length] { + let toTake = min(minPerLength, lengthPaths.count) + pruned.append(contentsOf: lengthPaths.prefix(toTake)) } + } - // Stop if we have enough - if pruned.count >= beamWidth * 2 { - break + // Second pass: fill remaining slots with best paths overall + if pruned.count < beamWidth { + let remaining = beamWidth - pruned.count + let prunedIds = Set(pruned.map { $0.map { $0.id.uuidString }.joined(separator: "-") }) + + // Get all paths not yet added, sorted by score + var additional = uniquePaths.filter { + !prunedIds.contains($0.map { $0.id.uuidString }.joined(separator: "-")) } + additional.sort { scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) } + + pruned.append(contentsOf: additional.prefix(remaining)) } // Final truncation diff --git a/SportsTime/Planning/Engine/RouteFilters.swift b/SportsTime/Planning/Engine/RouteFilters.swift new file mode 100644 index 0000000..def90e4 --- /dev/null +++ b/SportsTime/Planning/Engine/RouteFilters.swift @@ -0,0 +1,60 @@ +// +// RouteFilters.swift +// SportsTime +// +// Filters itinerary results based on user preferences. +// Applied in TripPlanningEngine AFTER scenario planners return. +// + +import Foundation +import CoreLocation + +enum RouteFilters { + + // MARK: - Repeat Cities Filter + + /// Filter itinerary options that violate repeat city rules. + /// When allowRepeatCities=false, each city must be visited on exactly ONE day. + static func filterRepeatCities( + _ options: [ItineraryOption], + allow: Bool + ) -> [ItineraryOption] { + guard !allow else { return options } + return options.filter { !hasRepeatCityViolation($0) } + } + + /// Check if an itinerary visits any city on multiple days. + static func hasRepeatCityViolation(_ option: ItineraryOption) -> Bool { + let calendar = Calendar.current + var cityDays: [String: Set] = [:] + + for stop in option.stops { + let city = stop.city + let day = calendar.startOfDay(for: stop.arrivalDate) + cityDays[city, default: []].insert(day) + } + + // Violation if any city has more than 1 day + return cityDays.values.contains(where: { $0.count > 1 }) + } + + /// Get cities that are visited on multiple days (for error reporting). + static func findRepeatCities(in options: [ItineraryOption]) -> [String] { + var violatingCities = Set() + let calendar = Calendar.current + + for option in options { + var cityDays: [String: Set] = [:] + for stop in option.stops { + let day = calendar.startOfDay(for: stop.arrivalDate) + cityDays[stop.city, default: []].insert(day) + } + for (city, days) in cityDays where days.count > 1 { + violatingCities.insert(city) + } + } + + return Array(violatingCities).sorted() + } + +} diff --git a/SportsTime/Planning/Engine/ScenarioAPlanner.swift b/SportsTime/Planning/Engine/ScenarioAPlanner.swift index f43ffe3..6bce5b7 100644 --- a/SportsTime/Planning/Engine/ScenarioAPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioAPlanner.swift @@ -60,12 +60,24 @@ final class ScenarioAPlanner: ScenarioPlanner { } // ────────────────────────────────────────────────────────────────── - // Step 2: Filter games within date range + // Step 2: Filter games within date range and selected regions // ────────────────────────────────────────────────────────────────── // Get all games that fall within the user's travel dates. // Sort by start time so we visit them in chronological order. + let selectedRegions = request.preferences.selectedRegions let gamesInRange = request.allGames - .filter { dateRange.contains($0.startTime) } + .filter { game in + // Must be in date range + guard dateRange.contains(game.startTime) else { return false } + + // Must be in selected region (if regions specified) + if !selectedRegions.isEmpty { + guard let stadium = request.stadiums[game.stadiumId] else { return false } + let gameRegion = Region.classify(longitude: stadium.coordinate.longitude) + return selectedRegions.contains(gameRegion) + } + return true + } .sorted { $0.startTime < $1.startTime } // No games? Nothing to plan. @@ -91,11 +103,32 @@ final class ScenarioAPlanner: ScenarioPlanner { // We explore ALL valid combinations and return multiple options. // Uses GameDAGRouter for polynomial-time beam search. // - let validRoutes = GameDAGRouter.findAllSensibleRoutes( + // Run beam search BOTH globally AND per-region to get diverse routes: + // - Global search finds cross-region routes + // - Per-region search ensures we have good regional options too + // Travel style filtering happens at UI layer. + // + var validRoutes: [[Game]] = [] + + // Global beam search (finds cross-region routes) + let globalRoutes = GameDAGRouter.findAllSensibleRoutes( from: gamesInRange, stadiums: request.stadiums, + allowRepeatCities: request.preferences.allowRepeatCities, stopBuilder: buildStops ) + validRoutes.append(contentsOf: globalRoutes) + + // Per-region beam search (ensures good regional options) + let regionalRoutes = findRoutesPerRegion( + games: gamesInRange, + stadiums: request.stadiums, + allowRepeatCities: request.preferences.allowRepeatCities + ) + validRoutes.append(contentsOf: regionalRoutes) + + // Deduplicate routes (same game IDs) + validRoutes = deduplicateRoutes(validRoutes) print("🔍 ScenarioA: gamesInRange=\(gamesInRange.count), validRoutes=\(validRoutes.count)") if let firstRoute = validRoutes.first { @@ -201,11 +234,10 @@ final class ScenarioAPlanner: ScenarioPlanner { let rankedOptions = ItineraryOption.sortByLeisure( itineraryOptions, - leisureLevel: leisureLevel, - limit: request.preferences.maxTripOptions + leisureLevel: leisureLevel ) - print("🔍 ScenarioA: Returning \(rankedOptions.count) options after sorting (limit=\(request.preferences.maxTripOptions))") + print("🔍 ScenarioA: Returning \(rankedOptions.count) options after sorting") if let first = rankedOptions.first { print("🔍 ScenarioA: First option has \(first.stops.count) stops") } @@ -310,4 +342,69 @@ final class ScenarioAPlanner: ScenarioPlanner { ) } + // MARK: - Route Deduplication + + /// Removes duplicate routes (routes with identical game IDs). + private func deduplicateRoutes(_ routes: [[Game]]) -> [[Game]] { + var seen = Set() + var unique: [[Game]] = [] + + for route in routes { + let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-") + if !seen.contains(key) { + seen.insert(key) + unique.append(route) + } + } + + return unique + } + + // MARK: - Regional Route Finding + + /// Finds routes by running beam search separately for each geographic region. + /// This ensures we get diverse options from East, Central, and West coasts. + private func findRoutesPerRegion( + games: [Game], + stadiums: [UUID: Stadium], + allowRepeatCities: Bool + ) -> [[Game]] { + // Partition games by region + var gamesByRegion: [Region: [Game]] = [:] + + for game in games { + guard let stadium = stadiums[game.stadiumId] else { continue } + let coord = stadium.coordinate + let region = Region.classify(longitude: coord.longitude) + // Only consider actual regions, not cross-country + if region != .crossCountry { + gamesByRegion[region, default: []].append(game) + } + } + + print("🔍 ScenarioA Regional: Partitioned \(games.count) games into \(gamesByRegion.count) regions") + for (region, regionGames) in gamesByRegion { + print(" \(region.shortName): \(regionGames.count) games") + } + + // Run beam search for each region + var allRoutes: [[Game]] = [] + + for (region, regionGames) in gamesByRegion { + guard !regionGames.isEmpty else { continue } + + let regionRoutes = GameDAGRouter.findAllSensibleRoutes( + from: regionGames, + stadiums: stadiums, + allowRepeatCities: allowRepeatCities, + stopBuilder: buildStops + ) + + print("🔍 ScenarioA Regional: \(region.shortName) produced \(regionRoutes.count) routes") + allRoutes.append(contentsOf: regionRoutes) + } + + return allRoutes + } + } diff --git a/SportsTime/Planning/Engine/ScenarioBPlanner.swift b/SportsTime/Planning/Engine/ScenarioBPlanner.swift index 7fba8da..dd8a9e9 100644 --- a/SportsTime/Planning/Engine/ScenarioBPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioBPlanner.swift @@ -85,12 +85,25 @@ final class ScenarioBPlanner: ScenarioPlanner { // Step 3: For each date range, find routes with anchors // ────────────────────────────────────────────────────────────────── let anchorGameIds = Set(selectedGames.map { $0.id }) + let selectedRegions = request.preferences.selectedRegions var allItineraryOptions: [ItineraryOption] = [] for dateRange in dateRanges { - // Find all games in this date range + // Find all games in this date range and selected regions let gamesInRange = request.allGames - .filter { dateRange.contains($0.startTime) } + .filter { game in + // Must be in date range + guard dateRange.contains(game.startTime) else { return false } + + // Must be in selected region (if regions specified) + // Note: Anchor games are always included regardless of region + if !selectedRegions.isEmpty && !anchorGameIds.contains(game.id) { + guard let stadium = request.stadiums[game.stadiumId] else { return false } + let gameRegion = Region.classify(longitude: stadium.coordinate.longitude) + return selectedRegions.contains(gameRegion) + } + return true + } .sorted { $0.startTime < $1.startTime } // Skip if no games (shouldn't happen if date range is valid) @@ -104,12 +117,30 @@ final class ScenarioBPlanner: ScenarioPlanner { // Find all sensible routes that include the anchor games // Uses GameDAGRouter for polynomial-time beam search - let validRoutes = GameDAGRouter.findAllSensibleRoutes( + // Run BOTH global and per-region search for diverse routes + var validRoutes: [[Game]] = [] + + // Global beam search (finds cross-region routes) + let globalRoutes = GameDAGRouter.findAllSensibleRoutes( from: gamesInRange, stadiums: request.stadiums, anchorGameIds: anchorGameIds, + allowRepeatCities: request.preferences.allowRepeatCities, stopBuilder: buildStops ) + validRoutes.append(contentsOf: globalRoutes) + + // Per-region beam search (ensures good regional options) + let regionalRoutes = findRoutesPerRegion( + games: gamesInRange, + stadiums: request.stadiums, + anchorGameIds: anchorGameIds, + allowRepeatCities: request.preferences.allowRepeatCities + ) + validRoutes.append(contentsOf: regionalRoutes) + + // Deduplicate + validRoutes = deduplicateRoutes(validRoutes) // Build itineraries for each valid route for routeGames in validRoutes { @@ -164,8 +195,7 @@ final class ScenarioBPlanner: ScenarioPlanner { let leisureLevel = request.preferences.leisureLevel let rankedOptions = ItineraryOption.sortByLeisure( allItineraryOptions, - leisureLevel: leisureLevel, - limit: request.preferences.maxTripOptions + leisureLevel: leisureLevel ) return .success(Array(rankedOptions)) @@ -354,4 +384,84 @@ final class ScenarioBPlanner: ScenarioPlanner { ) } + // MARK: - Regional Route Finding + + /// Finds routes by running beam search separately for each geographic region. + /// This ensures we get diverse options from East, Central, and West coasts. + /// For Scenario B, routes must still contain all anchor games. + private func findRoutesPerRegion( + games: [Game], + stadiums: [UUID: Stadium], + anchorGameIds: Set, + allowRepeatCities: Bool + ) -> [[Game]] { + // First, determine which region(s) the anchor games are in + var anchorRegions = Set() + for game in games where anchorGameIds.contains(game.id) { + guard let stadium = stadiums[game.stadiumId] else { continue } + let coord = stadium.coordinate + let region = Region.classify(longitude: coord.longitude) + if region != .crossCountry { + anchorRegions.insert(region) + } + } + + // Partition all games by region + var gamesByRegion: [Region: [Game]] = [:] + for game in games { + guard let stadium = stadiums[game.stadiumId] else { continue } + let coord = stadium.coordinate + let region = Region.classify(longitude: coord.longitude) + if region != .crossCountry { + gamesByRegion[region, default: []].append(game) + } + } + + print("🔍 ScenarioB Regional: Anchor games in regions: \(anchorRegions.map { $0.shortName })") + + // Run beam search for each region that has anchor games + // (Other regions without anchor games would produce routes that don't satisfy anchors) + var allRoutes: [[Game]] = [] + + for region in anchorRegions { + guard let regionGames = gamesByRegion[region], !regionGames.isEmpty else { continue } + + // Get anchor games in this region + let regionAnchorIds = anchorGameIds.filter { anchorId in + regionGames.contains { $0.id == anchorId } + } + + let regionRoutes = GameDAGRouter.findAllSensibleRoutes( + from: regionGames, + stadiums: stadiums, + anchorGameIds: regionAnchorIds, + allowRepeatCities: allowRepeatCities, + stopBuilder: buildStops + ) + + print("🔍 ScenarioB Regional: \(region.shortName) produced \(regionRoutes.count) routes") + allRoutes.append(contentsOf: regionRoutes) + } + + return allRoutes + } + + // MARK: - Route Deduplication + + /// Removes duplicate routes (routes with identical game IDs). + private func deduplicateRoutes(_ routes: [[Game]]) -> [[Game]] { + var seen = Set() + var unique: [[Game]] = [] + + for route in routes { + let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-") + if !seen.contains(key) { + seen.insert(key) + unique.append(route) + } + } + + return unique + } + } diff --git a/SportsTime/Planning/Engine/ScenarioCPlanner.swift b/SportsTime/Planning/Engine/ScenarioCPlanner.swift index 88c314a..7b7a0d0 100644 --- a/SportsTime/Planning/Engine/ScenarioCPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioCPlanner.swift @@ -261,8 +261,7 @@ final class ScenarioCPlanner: ScenarioPlanner { let leisureLevel = request.preferences.leisureLevel let rankedOptions = ItineraryOption.sortByLeisure( allItineraryOptions, - leisureLevel: leisureLevel, - limit: request.preferences.maxTripOptions + leisureLevel: leisureLevel ) return .success(Array(rankedOptions)) diff --git a/SportsTime/Planning/Engine/TripPlanningEngine.swift b/SportsTime/Planning/Engine/TripPlanningEngine.swift index 1d99dc3..34384de 100644 --- a/SportsTime/Planning/Engine/TripPlanningEngine.swift +++ b/SportsTime/Planning/Engine/TripPlanningEngine.swift @@ -22,6 +22,41 @@ final class TripPlanningEngine { let planner = ScenarioPlannerFactory.planner(for: request) // Delegate to the scenario planner - return planner.plan(request: request) + let result = planner.plan(request: request) + + // Apply preference filters to successful results + return applyPreferenceFilters(to: result, request: request) + } + + // MARK: - Private + + /// Applies allowRepeatCities filter after scenario planners return. + /// Note: Region filtering is done during game selection in scenario planners. + private func applyPreferenceFilters( + to result: ItineraryResult, + request: PlanningRequest + ) -> ItineraryResult { + guard case .success(let originalOptions) = result else { + return result + } + + var options = originalOptions + + // Filter repeat cities (this is enforced during beam search, but double-check here) + options = RouteFilters.filterRepeatCities( + options, + allow: request.preferences.allowRepeatCities + ) + + if options.isEmpty && !request.preferences.allowRepeatCities { + let violatingCities = RouteFilters.findRepeatCities(in: originalOptions) + return .failure(PlanningFailure( + reason: .repeatCityViolation(cities: violatingCities) + )) + } + + // Region filtering is applied during game selection in scenario planners + + return .success(options) } } diff --git a/SportsTime/Planning/Models/PlanningModels.swift b/SportsTime/Planning/Models/PlanningModels.swift index 8b6459a..152748c 100644 --- a/SportsTime/Planning/Models/PlanningModels.swift +++ b/SportsTime/Planning/Models/PlanningModels.swift @@ -35,6 +35,7 @@ struct PlanningFailure: Error { case travelSegmentMissing case constraintsUnsatisfiable case geographicBacktracking + case repeatCityViolation(cities: [String]) static func == (lhs: FailureReason, rhs: FailureReason) -> Bool { switch (lhs, rhs) { @@ -50,6 +51,8 @@ struct PlanningFailure: Error { return true case (.dateRangeViolation(let g1), .dateRangeViolation(let g2)): return g1.map { $0.id } == g2.map { $0.id } + case (.repeatCityViolation(let c1), .repeatCityViolation(let c2)): + return c1 == c2 default: return false } @@ -74,6 +77,10 @@ struct PlanningFailure: Error { case .travelSegmentMissing: return "Travel segment could not be created" case .constraintsUnsatisfiable: return "Cannot satisfy all trip constraints" case .geographicBacktracking: return "Route requires excessive backtracking" + case .repeatCityViolation(let cities): + let cityList = cities.prefix(3).joined(separator: ", ") + let suffix = cities.count > 3 ? " and \(cities.count - 3) more" : "" + return "Cannot visit cities on multiple days: \(cityList)\(suffix)" } } } @@ -182,8 +189,7 @@ struct ItineraryOption: Identifiable { /// - Parameters: /// - options: The itinerary options to sort /// - leisureLevel: The user's leisure preference - /// - limit: Maximum number of options to return (default 10) - /// - Returns: Sorted and ranked options + /// - Returns: Sorted and ranked options (all options, no limit) /// /// Sorting behavior: /// - Packed: Most games first, then least driving @@ -191,8 +197,7 @@ struct ItineraryOption: Identifiable { /// - Relaxed: Least driving first, then fewer games static func sortByLeisure( _ options: [ItineraryOption], - leisureLevel: LeisureLevel, - limit: Int = 10 + leisureLevel: LeisureLevel ) -> [ItineraryOption] { let sorted = options.sorted { a, b in let aGames = a.totalGames @@ -220,8 +225,8 @@ struct ItineraryOption: Identifiable { } } - // Re-rank after sorting - return Array(sorted.prefix(limit)).enumerated().map { index, option in + // Re-rank after sorting (no limit - return all options) + return sorted.enumerated().map { index, option in ItineraryOption( rank: index + 1, stops: option.stops,