fplkit 1.3.0__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/SKILL.md +55 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/references/rules.md +13 -0
- fplkit-1.4.0/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +466 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.claude/output-styles/fpl-mate.md +18 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/AGENTS.md +3 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/CHANGELOG.md +25 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/CLAUDE.md +3 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/PKG-INFO +2 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/docs/architecture.md +4 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/docs/command-reference.md +4 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/_version.py +2 -2
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/data/fixture.py +6 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/orchestration/report.py +8 -7
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_helpers.py +11 -3
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_league_recap_data.py +312 -91
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_league_recap_types.py +14 -5
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_review_classic.py +14 -2
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_review_summarisation.py +194 -28
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/fixtures.py +2 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/league_recap.py +6 -4
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/preview.py +9 -11
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/status.py +6 -2
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/transfer_eval.py +1 -0
- fplkit-1.4.0/fpl_cli/config/fixture_predictions.yaml +10 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/paths.py +0 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/prompts/league_recap.py +49 -2
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/prompts/review.py +224 -22
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/scraper/fpl_prices.py +4 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/fixture_predictions.py +2 -2
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/templates/gw_league_recap.md.j2 +1 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/templates/gw_review.md.j2 +2 -2
- fplkit-1.4.0/fpl_cli/utils/time.py +71 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/pyproject.toml +1 -0
- fplkit-1.4.0/tests/fixtures/validate_draft_waivers/recs_clean.md +20 -0
- fplkit-1.4.0/tests/fixtures/validate_draft_waivers/squad_grid.json +21 -0
- fplkit-1.4.0/tests/fixtures/validate_draft_waivers/waivers.json +12 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_preview.py +2 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_review.py +32 -1
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_status.py +129 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_league_recap.py +572 -14
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_report_agent.py +13 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_review.py +656 -4
- fplkit-1.4.0/tests/test_utils_time.py +85 -0
- fplkit-1.4.0/tests/test_validate_draft_waivers.py +748 -0
- fplkit-1.3.0/config/fixture_predictions.yaml +0 -41
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/README.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/TOOLS.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.claude/settings.json +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.env.example +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.github/workflows/ci.yml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.github/workflows/release.yml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/.gitignore +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/LICENSE +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/README.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/cliff.toml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/config/team_managers.yaml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/config/team_ratings.yaml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/docs/custom-analysis.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/docs/fpl-rules.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/common.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/fpl.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/understat.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_context.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/_review_draft.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/league.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/player.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/review.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/squad.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/stats.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/constants.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/player.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/team.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/models/types.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/season.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/player_scoring.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/fpl_cli/utils/text.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/pyrightconfig.json +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/requirements.lock +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/__init__.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/conftest.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_action.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_analysis.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_base.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_data.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_data_price.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_allocate.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_client.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_core_insights.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_draft.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_football_data.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_historical.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_understat.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_api_vaastav.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_allocate.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_banner.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_captain.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_chips.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_differentials.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_fdr.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_format.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_history.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_init.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_json.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_player.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_price_history.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_squad.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_stats.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_targets.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_cli_xg.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_common.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_common_scoring.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_fines.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_fines_config.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_fpl_prices.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_init.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_init_fines.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_matchup_service.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_models.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_paths.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_player_prior.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_player_resolution.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_player_scoring.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_providers.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_recommendations_parser.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_scraper.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_season.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_sell_prices.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_stats.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_team_ratings.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-1.3.0 → fplkit-1.4.0}/tests/test_utils_text.py +0 -0
|
@@ -414,12 +414,17 @@ Proceed immediately (non-interactive).
|
|
|
414
414
|
> **Data (JSON):**
|
|
415
415
|
> - Status: {A1 output}
|
|
416
416
|
> - pFDR: {B1 output}
|
|
417
|
+
>
|
|
418
|
+
> **WAIVER POOL IS AUTHORITATIVE:** `fpl waivers` output is the only source for available players. All other data (stats, form tables, squad context) is for analysis only. Never recommend a claim not present in the waivers output — Phase D1 will flag pool misses as a warning. Cross-position recommendations (e.g. dropping a MID to claim a DEF) are structurally illegal and will be blocked by Phase D1.
|
|
419
|
+
>
|
|
417
420
|
> - Waivers: {B3 output}
|
|
418
421
|
> - Squad: {B4 output}
|
|
419
422
|
> - Stats: {stats_form}, {stats_transfer_momentum}, {stats_mid_xgi}, {stats_fwd_xgi}, {stats_def_clean_sheets} (from B8)
|
|
420
423
|
>
|
|
421
424
|
> <!-- ADAPT: Add your own supplementary data sources here (newsletters, external reports) -->
|
|
422
425
|
>
|
|
426
|
+
> Additionally enforce: every waiver swap must be position-for-position (MID out → MID in, DEF out → DEF in, etc.). Cross-position swaps are illegal under FPL Draft rules.
|
|
427
|
+
>
|
|
423
428
|
> Produce the **Draft** section of the output template.
|
|
424
429
|
|
|
425
430
|
### C2.5 -- Transfer/Waiver Evaluation
|
|
@@ -475,6 +480,56 @@ Present a brief summary to the user:
|
|
|
475
480
|
|
|
476
481
|
---
|
|
477
482
|
|
|
483
|
+
## Phase D1: Draft Validation (draft format only - skip if format is "classic")
|
|
484
|
+
|
|
485
|
+
_Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
486
|
+
|
|
487
|
+
1. Write Phase B's waivers JSON (from orchestrator context, B3 output) to `/tmp/gw-prep-waivers-{N}.json`.
|
|
488
|
+
2. Write Phase B's draft squad-grid JSON (from orchestrator context, B4 draft output) to `/tmp/gw-prep-squad-grid-{N}.json`.
|
|
489
|
+
- For draft-only runs, this is the `fpl squad grid --format json` output from B4.
|
|
490
|
+
- For `both` format runs, this is the `fpl squad grid --draft --format json` output from B4.
|
|
491
|
+
3. Run:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
cd "$FPL_CLI_DIR" && source .venv/bin/activate && python "$FPL_CLI_DIR/.agents/skills/gw-prep/scripts/validate_draft_waivers.py" \
|
|
495
|
+
--recommendations-file "[YOUR_OUTPUT_DIR]/gw{N}-recommendations.md" \
|
|
496
|
+
--waivers-json /tmp/gw-prep-waivers-{N}.json \
|
|
497
|
+
--squad-grid-json /tmp/gw-prep-squad-grid-{N}.json
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Parse stdout as JSON: `{"ok": bool, "flags": [...], "warnings": [...]}`.
|
|
501
|
+
If the script cannot be found or exits non-zero unexpectedly, emit a warning and proceed to Phase E (fail-open for infrastructure errors, fail-closed only for confirmed rule violations).
|
|
502
|
+
|
|
503
|
+
4. **Managed block update:** locate the sentinel block in the recommendations file:
|
|
504
|
+
```
|
|
505
|
+
<!-- phase-d1:start -->
|
|
506
|
+
...
|
|
507
|
+
<!-- phase-d1:end -->
|
|
508
|
+
```
|
|
509
|
+
- If flags or warnings are non-empty: replace (or insert) the block immediately after the `## Draft` (or `## Draft League`) heading line with:
|
|
510
|
+
```
|
|
511
|
+
<!-- phase-d1:start -->
|
|
512
|
+
> ⚠️ Draft validation:
|
|
513
|
+
> - {one line per flag/warning, e.g. "Row 1: cross-position-claim (Drop: João Pedro FWD → Claim: Pedro Porro DEF)"}
|
|
514
|
+
<!-- phase-d1:end -->
|
|
515
|
+
```
|
|
516
|
+
- If both are empty: remove the block entirely (including sentinels) if present.
|
|
517
|
+
|
|
518
|
+
5. **Split posture:**
|
|
519
|
+
- Any flag with `type == "cross-position-claim"` present → emit a **red error** in chat:
|
|
520
|
+
> ❌ Phase D1 blocked: `gw{N}-recommendations.md` contains illegal cross-position waiver swap(s):
|
|
521
|
+
> - Row {N}: Drop `{drop}` ({drop_position}) → Claim `{claim}` ({claim_position})
|
|
522
|
+
> Edit the file to fix the flagged row(s), then re-run `/gw-prep` to continue.
|
|
523
|
+
|
|
524
|
+
**Stop the pipeline.** Do not proceed to Phase E. Do not mark the run as complete.
|
|
525
|
+
- Else (only warnings and/or `waiver-not-in-pool` flags) → emit an in-chat warning:
|
|
526
|
+
> ⚠️ Phase D1: {N} waiver pool miss(es) in `gw{N}-recommendations.md` — see managed block in the Draft section. Claims may be normaliser false positives; verify before submitting waivers.
|
|
527
|
+
|
|
528
|
+
Proceed to Phase E.
|
|
529
|
+
- No flags or warnings → silent continue to Phase E.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
478
533
|
## Phase E: Post-write Validation (embed-mode only)
|
|
479
534
|
|
|
480
535
|
_Skip unless `squad_builder_result == "embed"`. Transfer and rederive runs do not produce a `### Classic Squad` block._
|
|
@@ -44,6 +44,8 @@ When `mode = "squad-builder"`:
|
|
|
44
44
|
3. **Availability likelihood** -- consider whether high-value targets are likely to be claimed by opponents with higher waiver priority.
|
|
45
45
|
4. **Fixture run** -- as with classic, weight the next 3 gameweeks of fixtures rather than just the immediate one.
|
|
46
46
|
5. **Maximum 5 waiver suggestions** -- for each: player to drop, player to claim, positional context, and fixture rationale.
|
|
47
|
+
6. **Position-for-position only** -- FPL Draft squads are locked at 2 GK / 5 DEF / 5 MID / 3 FWD. Every waiver swap must be position-for-position (e.g. MID out → MID in). Cross-position swaps are structurally illegal and will be rejected by Phase D1.
|
|
48
|
+
7. **Waiver pool discipline** -- only recommend players returned by `fpl waivers --format json`. Players appearing in other data sources (e.g. `fpl stats`) are not necessarily available; never recommend a claim not in the waiver pool.
|
|
47
49
|
|
|
48
50
|
## Transfer/Waiver Evaluation Script
|
|
49
51
|
|
|
@@ -72,6 +74,10 @@ Flag the following situations prominently:
|
|
|
72
74
|
- **Minutes risk**: started fewer than 2 of last 4 matches
|
|
73
75
|
- **Price crash**: lost 0.2+ in value over the last 5 days
|
|
74
76
|
|
|
77
|
+
### Grounding rule (no fabricated stats)
|
|
78
|
+
|
|
79
|
+
Momentum Alert bullets must only cite numbers that appear in the CLI/stats output already inlined into the sub-agent prompt. Do not invent derivative framings like "three straight attacking returns", "X goals in Y games", "scored in N consecutive", or "back-to-back doubles" unless the underlying per-GW breakdown is visible in the provided data. When in doubt, stick to the four defined trigger types above plus the raw form number, price delta, minutes count, and injury flag percentage — these are always available. Prose beyond those fields is a fabrication risk and must be cut.
|
|
80
|
+
|
|
75
81
|
## pFDR (Positional FDR) Analysis
|
|
76
82
|
|
|
77
83
|
For each position (GK, DEF, MID, FWD):
|
|
@@ -80,6 +86,13 @@ For each position (GK, DEF, MID, FWD):
|
|
|
80
86
|
2. Identify the 3 worst fixture runs (highest average pFDR).
|
|
81
87
|
3. Cross-reference with the user's current squad to surface mismatches (e.g. holding defenders with terrible upcoming fixtures).
|
|
82
88
|
|
|
89
|
+
### pFDR Overview row format
|
|
90
|
+
|
|
91
|
+
- **One row per team, not per player.** Multiple squad players from the same team (e.g. Raya + Gabriel at ARS) may share a row because their fixtures are identical.
|
|
92
|
+
- **Never combine players from different teams into a single row.** Their fixture paths diverge after GW1 of the window and the downstream cells cannot be populated. If two same-position players are on different teams, give them separate rows.
|
|
93
|
+
- **Every cell in GW{N}..GW{N+4} must be populated** with either opponent+pFDR or `BLANK` (for blank gameweeks). A `-` in any fixture cell is a bug: re-run `fpl fixtures -g {gw}` for the missing gameweeks.
|
|
94
|
+
- **Avg column** = mean pFDR across the 5 GWs, treating BLANK as excluded from the denominator.
|
|
95
|
+
|
|
83
96
|
## Selection Requirements
|
|
84
97
|
|
|
85
98
|
Every recommendation must include:
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate Draft waiver recommendations against the live waiver pool and squad-grid.
|
|
3
|
+
|
|
4
|
+
Reads a gw{N}-recommendations.md file, locates the ## Draft (or ## Draft League)
|
|
5
|
+
section, parses the waiver table, and cross-checks each row against:
|
|
6
|
+
- waivers JSON: claim must be present in the pool (waiver-not-in-pool)
|
|
7
|
+
- squad-grid JSON: drop position must match claim position (cross-position-claim)
|
|
8
|
+
|
|
9
|
+
Emits JSON to stdout: {"ok": bool, "flags": [...], "warnings": [...]}
|
|
10
|
+
Exit code is always 0; the orchestrator (Phase D1) interprets flag types for posture.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 validate_draft_waivers.py \\
|
|
14
|
+
--recommendations-file path/to/gw34-recommendations.md \\
|
|
15
|
+
--waivers-json /tmp/waivers.json \\
|
|
16
|
+
--squad-grid-json /tmp/squad-grid.json
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import unicodedata
|
|
26
|
+
from typing import TypedDict
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Types
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Flag(TypedDict, total=False):
|
|
34
|
+
type: str
|
|
35
|
+
row: int
|
|
36
|
+
claim: str
|
|
37
|
+
drop: str
|
|
38
|
+
drop_position: str
|
|
39
|
+
claim_position: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Warning(TypedDict, total=False):
|
|
43
|
+
type: str
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ValidatorResult(TypedDict):
|
|
48
|
+
ok: bool
|
|
49
|
+
flags: list[Flag]
|
|
50
|
+
warnings: list[Warning]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PoolEntry(TypedDict):
|
|
54
|
+
player_name: str
|
|
55
|
+
position: str
|
|
56
|
+
team_short: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SquadEntry(TypedDict):
|
|
60
|
+
player: str
|
|
61
|
+
position: str
|
|
62
|
+
team: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Normalisation
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
_APOSTROPHE_RE = re.compile(r"[‘’ʼʹ`]")
|
|
70
|
+
_HYPHEN_RE = re.compile(r"[–—‒‐‑]")
|
|
71
|
+
_TEAM_SUFFIX_RE = re.compile(r"\s*\([A-Z]{2,4}\)\s*$")
|
|
72
|
+
_MARKDOWN_WRAP_RE = re.compile(r"^[\*_`]+|[\*_`]+$")
|
|
73
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalise(name: str) -> str:
|
|
77
|
+
"""Return a canonical key for name matching."""
|
|
78
|
+
s = name.strip()
|
|
79
|
+
# strip markdown bold/italic/code wrapping
|
|
80
|
+
s = _MARKDOWN_WRAP_RE.sub("", s).strip()
|
|
81
|
+
# strip trailing team suffix like "(CHE)" or "(TOT)"
|
|
82
|
+
s = _TEAM_SUFFIX_RE.sub("", s).strip()
|
|
83
|
+
# NFKD decompose then drop combining marks
|
|
84
|
+
s = unicodedata.normalize("NFKD", s)
|
|
85
|
+
s = "".join(c for c in s if not unicodedata.category(c).startswith("M"))
|
|
86
|
+
# fold apostrophe variants
|
|
87
|
+
s = _APOSTROPHE_RE.sub("'", s)
|
|
88
|
+
# fold hyphen/dash variants
|
|
89
|
+
s = _HYPHEN_RE.sub("-", s)
|
|
90
|
+
# fold dots to space (handles J.Pedro vs J Pedro, B.Fernandes vs B Fernandes)
|
|
91
|
+
s = s.replace(".", " ")
|
|
92
|
+
# casefold
|
|
93
|
+
s = s.casefold()
|
|
94
|
+
# collapse whitespace
|
|
95
|
+
s = _WHITESPACE_RE.sub(" ", s).strip()
|
|
96
|
+
return s
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_PLACEHOLDER_VALUES = frozenset({"-", "—", "–", "", "hold", "(depth)", "tbd", "n/a"})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_placeholder(raw: str) -> bool:
|
|
103
|
+
key = normalise(raw)
|
|
104
|
+
return key in _PLACEHOLDER_VALUES or (key.startswith("(") and key.endswith(")"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Pool / squad builders
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _shape_check_squad(data: dict) -> bool:
|
|
113
|
+
"""Return True if squad-grid JSON has the expected shape."""
|
|
114
|
+
try:
|
|
115
|
+
rows = data["data"]
|
|
116
|
+
if not rows:
|
|
117
|
+
return True
|
|
118
|
+
sample = rows[0]
|
|
119
|
+
return "player" in sample and "position" in sample
|
|
120
|
+
except (KeyError, IndexError, TypeError):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _shape_check_waivers(data: dict) -> bool:
|
|
125
|
+
"""Return True if waivers JSON has the expected shape."""
|
|
126
|
+
try:
|
|
127
|
+
targets = data["data"]["top_targets"]
|
|
128
|
+
if not isinstance(targets, list):
|
|
129
|
+
return False
|
|
130
|
+
if not targets:
|
|
131
|
+
return True
|
|
132
|
+
sample = targets[0]
|
|
133
|
+
return "player_name" in sample and "position" in sample
|
|
134
|
+
except (KeyError, TypeError):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_pool(waivers_data: dict) -> dict[tuple[str, str], PoolEntry]:
|
|
139
|
+
"""Build {(position, normalised_name) -> entry} from waivers JSON."""
|
|
140
|
+
pool: dict[tuple[str, str], PoolEntry] = {}
|
|
141
|
+
for entry in waivers_data.get("data", {}).get("top_targets", []):
|
|
142
|
+
pos = entry.get("position", "")
|
|
143
|
+
key = (pos, normalise(entry.get("player_name", "")))
|
|
144
|
+
pool[key] = entry
|
|
145
|
+
return pool
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_squad(squad_data: dict) -> dict[tuple[str, str], SquadEntry]:
|
|
149
|
+
"""Build {(position, normalised_name) -> entry} from squad-grid JSON."""
|
|
150
|
+
squad: dict[tuple[str, str], SquadEntry] = {}
|
|
151
|
+
for entry in squad_data.get("data", []):
|
|
152
|
+
pos = entry.get("position", "")
|
|
153
|
+
key = (pos, normalise(entry.get("player", "")))
|
|
154
|
+
squad[key] = entry
|
|
155
|
+
return squad
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Section / table parsing
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
_DRAFT_HEADING_RE = re.compile(r"^## Draft(?: League)?\s*$")
|
|
163
|
+
_WAIVER_SUBHEADING_RE = re.compile(r"^###\s+Waiver Recommendations\s*$", re.IGNORECASE)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def locate_draft_section(lines: list[str]) -> tuple[int, int] | None:
|
|
167
|
+
"""Return (start, end) line indices for the ## Draft / ## Draft League section."""
|
|
168
|
+
start: int | None = None
|
|
169
|
+
for i, line in enumerate(lines):
|
|
170
|
+
if _DRAFT_HEADING_RE.match(line):
|
|
171
|
+
start = i
|
|
172
|
+
break
|
|
173
|
+
if start is None:
|
|
174
|
+
return None
|
|
175
|
+
# end is next ## heading or EOF
|
|
176
|
+
for i in range(start + 1, len(lines)):
|
|
177
|
+
if re.match(r"^## \S", lines[i]):
|
|
178
|
+
return (start, i)
|
|
179
|
+
return (start, len(lines))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def locate_waiver_subsection(section_lines: list[str]) -> list[str]:
|
|
183
|
+
"""Return the slice of section_lines bounded by ### Waiver Recommendations.
|
|
184
|
+
|
|
185
|
+
Bounds end at the next ### or ## heading. Falls back to the full section if
|
|
186
|
+
the subheading is absent (older report formats).
|
|
187
|
+
"""
|
|
188
|
+
start: int | None = None
|
|
189
|
+
for i, line in enumerate(section_lines):
|
|
190
|
+
if _WAIVER_SUBHEADING_RE.match(line):
|
|
191
|
+
start = i + 1
|
|
192
|
+
break
|
|
193
|
+
if start is None:
|
|
194
|
+
return section_lines
|
|
195
|
+
for i in range(start, len(section_lines)):
|
|
196
|
+
if re.match(r"^##+\s+\S", section_lines[i]):
|
|
197
|
+
return section_lines[start:i]
|
|
198
|
+
return section_lines[start:]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def parse_waiver_table(
|
|
202
|
+
section_lines: list[str],
|
|
203
|
+
) -> tuple[list[str], list[dict[str, str]]] | None:
|
|
204
|
+
"""Parse the first markdown table in the section.
|
|
205
|
+
|
|
206
|
+
Returns (headers, rows) where rows are dicts keyed by header name,
|
|
207
|
+
or None if no table found.
|
|
208
|
+
"""
|
|
209
|
+
headers: list[str] | None = None
|
|
210
|
+
rows: list[dict[str, str]] = []
|
|
211
|
+
|
|
212
|
+
lines_iter = list(section_lines)
|
|
213
|
+
for idx, line in enumerate(lines_iter):
|
|
214
|
+
stripped = line.strip()
|
|
215
|
+
if not stripped.startswith("|"):
|
|
216
|
+
# End table at first non-pipe line after rows started (e.g. blank line
|
|
217
|
+
# before the next ### subsection's table).
|
|
218
|
+
if rows:
|
|
219
|
+
break
|
|
220
|
+
continue
|
|
221
|
+
if headers is None:
|
|
222
|
+
# first pipe-delimited line is the header row
|
|
223
|
+
parts = [p.strip() for p in stripped.strip("|").split("|")]
|
|
224
|
+
headers = parts
|
|
225
|
+
continue
|
|
226
|
+
# skip separator row
|
|
227
|
+
if re.match(r"^\|[-| ]+\|", stripped):
|
|
228
|
+
continue
|
|
229
|
+
# Detect a new header row stacked directly after the current table:
|
|
230
|
+
# current line is pipe-delimited and the next line is a separator. Break
|
|
231
|
+
# before consuming it as a data row.
|
|
232
|
+
if rows and idx + 1 < len(lines_iter):
|
|
233
|
+
next_stripped = lines_iter[idx + 1].strip()
|
|
234
|
+
if re.match(r"^\|[-| ]+\|", next_stripped):
|
|
235
|
+
break
|
|
236
|
+
parts = [p.strip() for p in stripped.strip("|").split("|")]
|
|
237
|
+
row: dict[str, str] = {}
|
|
238
|
+
for i, h in enumerate(headers):
|
|
239
|
+
row[h] = parts[i] if i < len(parts) else ""
|
|
240
|
+
rows.append(row)
|
|
241
|
+
|
|
242
|
+
if headers is None:
|
|
243
|
+
return None
|
|
244
|
+
return headers, rows
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Name resolution
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _resolve_in_pool(
|
|
253
|
+
name: str, position: str, pool: dict[tuple[str, str], PoolEntry]
|
|
254
|
+
) -> PoolEntry | None:
|
|
255
|
+
"""Exact match then last-token fallback, both position-scoped."""
|
|
256
|
+
norm = normalise(name)
|
|
257
|
+
# exact
|
|
258
|
+
if (position, norm) in pool:
|
|
259
|
+
return pool[(position, norm)]
|
|
260
|
+
# last-token fallback: check if norm equals last token of any pool entry at this pos
|
|
261
|
+
for (pos, pool_norm), entry in pool.items():
|
|
262
|
+
if pos != position:
|
|
263
|
+
continue
|
|
264
|
+
if pool_norm.split()[-1] == norm:
|
|
265
|
+
return entry
|
|
266
|
+
# also handle: pool entry is short, rec file has full name
|
|
267
|
+
if norm.split()[-1] == pool_norm:
|
|
268
|
+
return entry
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _resolve_in_squad(
|
|
273
|
+
name: str, squad: dict[tuple[str, str], SquadEntry]
|
|
274
|
+
) -> list[SquadEntry]:
|
|
275
|
+
"""Return all squad entries matching name (exact, else last-token fallback).
|
|
276
|
+
|
|
277
|
+
Exact matches short-circuit the fallback. Callers inspect the list length and
|
|
278
|
+
the set of positions to decide whether the match is unambiguous.
|
|
279
|
+
"""
|
|
280
|
+
norm = normalise(name)
|
|
281
|
+
exact = [entry for (_, squad_norm), entry in squad.items() if squad_norm == norm]
|
|
282
|
+
if exact:
|
|
283
|
+
return exact
|
|
284
|
+
return [
|
|
285
|
+
entry
|
|
286
|
+
for (_, squad_norm), entry in squad.items()
|
|
287
|
+
if squad_norm.split()[-1] == norm or norm.split()[-1] == squad_norm
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Core validation
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def validate_rows(
|
|
297
|
+
rows: list[dict[str, str]],
|
|
298
|
+
pool: dict[tuple[str, str], PoolEntry],
|
|
299
|
+
squad: dict[tuple[str, str], SquadEntry],
|
|
300
|
+
squad_shape_ok: bool,
|
|
301
|
+
waivers_shape_ok: bool = True,
|
|
302
|
+
) -> tuple[list[Flag], list[Warning]]:
|
|
303
|
+
flags: list[Flag] = []
|
|
304
|
+
warnings: list[Warning] = []
|
|
305
|
+
|
|
306
|
+
for i, row in enumerate(rows, start=1):
|
|
307
|
+
drop_raw = row.get("Drop", "")
|
|
308
|
+
claim_raw = row.get("Claim", "")
|
|
309
|
+
position = row.get("Position", "").strip()
|
|
310
|
+
|
|
311
|
+
if _is_placeholder(drop_raw) or _is_placeholder(claim_raw):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
# Resolve claim in pool (position-scoped). Skip when waivers shape is unknown —
|
|
315
|
+
# an empty pool would otherwise mark every real claim as a false positive.
|
|
316
|
+
claim_entry: PoolEntry | None = None
|
|
317
|
+
if waivers_shape_ok:
|
|
318
|
+
claim_entry = _resolve_in_pool(claim_raw, position, pool)
|
|
319
|
+
if claim_entry is None:
|
|
320
|
+
flags.append(
|
|
321
|
+
Flag(
|
|
322
|
+
type="waiver-not-in-pool",
|
|
323
|
+
row=i,
|
|
324
|
+
claim=claim_raw,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Resolve drop in squad (all positions, to find actual position)
|
|
329
|
+
if not squad_shape_ok:
|
|
330
|
+
continue
|
|
331
|
+
drop_candidates = _resolve_in_squad(drop_raw, squad)
|
|
332
|
+
if not drop_candidates:
|
|
333
|
+
warnings.append(Warning(type="drop-not-resolvable", name=drop_raw))
|
|
334
|
+
continue
|
|
335
|
+
drop_positions = {e["position"] for e in drop_candidates}
|
|
336
|
+
if len(drop_positions) > 1:
|
|
337
|
+
# Ambiguous last-token match across positions — can't determine drop_pos safely
|
|
338
|
+
warnings.append(Warning(type="drop-ambiguous-match", name=drop_raw))
|
|
339
|
+
continue
|
|
340
|
+
drop_entry = drop_candidates[0]
|
|
341
|
+
|
|
342
|
+
# Cross-position check
|
|
343
|
+
claim_pos = claim_entry["position"] if claim_entry else position
|
|
344
|
+
drop_pos = drop_entry["position"]
|
|
345
|
+
if drop_pos != claim_pos:
|
|
346
|
+
flags.append(
|
|
347
|
+
Flag(
|
|
348
|
+
type="cross-position-claim",
|
|
349
|
+
row=i,
|
|
350
|
+
drop=drop_raw,
|
|
351
|
+
drop_position=drop_pos,
|
|
352
|
+
claim=claim_raw,
|
|
353
|
+
claim_position=claim_pos,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return flags, warnings
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Entry point
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _run(recommendations_file: str, waivers_json: str, squad_grid_json: str) -> None:
|
|
366
|
+
flags: list[Flag] = []
|
|
367
|
+
warnings: list[Warning] = []
|
|
368
|
+
|
|
369
|
+
# Load waivers
|
|
370
|
+
with open(waivers_json, encoding="utf-8") as f:
|
|
371
|
+
waivers_data = json.load(f)
|
|
372
|
+
|
|
373
|
+
# Load squad-grid
|
|
374
|
+
with open(squad_grid_json, encoding="utf-8") as f:
|
|
375
|
+
squad_data = json.load(f)
|
|
376
|
+
|
|
377
|
+
# Shape checks
|
|
378
|
+
squad_shape_ok = _shape_check_squad(squad_data)
|
|
379
|
+
if not squad_shape_ok:
|
|
380
|
+
warnings.append(Warning(type="squad-grid-shape-unknown"))
|
|
381
|
+
|
|
382
|
+
waivers_shape_ok = _shape_check_waivers(waivers_data)
|
|
383
|
+
if not waivers_shape_ok:
|
|
384
|
+
warnings.append(Warning(type="waivers-json-shape-unknown"))
|
|
385
|
+
|
|
386
|
+
pool = build_pool(waivers_data) if waivers_shape_ok else {}
|
|
387
|
+
squad = build_squad(squad_data) if squad_shape_ok else {}
|
|
388
|
+
|
|
389
|
+
# Load recommendations
|
|
390
|
+
with open(recommendations_file, encoding="utf-8") as f:
|
|
391
|
+
content = f.read()
|
|
392
|
+
lines = content.split("\n")
|
|
393
|
+
|
|
394
|
+
# Locate draft section
|
|
395
|
+
section_range = locate_draft_section(lines)
|
|
396
|
+
if section_range is None:
|
|
397
|
+
warnings.append(Warning(type="draft-section-not-found"))
|
|
398
|
+
result: ValidatorResult = {"ok": True, "flags": [], "warnings": warnings}
|
|
399
|
+
json.dump(result, sys.stdout)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
start, end = section_range
|
|
403
|
+
section_lines = lines[start:end]
|
|
404
|
+
|
|
405
|
+
# Narrow to the ### Waiver Recommendations subsection so we don't ingest
|
|
406
|
+
# rows from the Starting XI / Bench tables that follow it.
|
|
407
|
+
waiver_lines = locate_waiver_subsection(section_lines)
|
|
408
|
+
|
|
409
|
+
# Parse waiver table
|
|
410
|
+
table = parse_waiver_table(waiver_lines)
|
|
411
|
+
if table is None:
|
|
412
|
+
warnings.append(Warning(type="waiver-table-not-found"))
|
|
413
|
+
result = {"ok": True, "flags": [], "warnings": warnings}
|
|
414
|
+
json.dump(result, sys.stdout)
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
headers, rows = table
|
|
418
|
+
if "Drop" not in headers or "Claim" not in headers:
|
|
419
|
+
warnings.append(Warning(type="drop-claim-columns-not-found"))
|
|
420
|
+
result = {"ok": True, "flags": [], "warnings": warnings}
|
|
421
|
+
json.dump(result, sys.stdout)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Validate rows
|
|
425
|
+
row_flags, row_warnings = validate_rows(
|
|
426
|
+
rows, pool, squad, squad_shape_ok, waivers_shape_ok
|
|
427
|
+
)
|
|
428
|
+
flags.extend(row_flags)
|
|
429
|
+
warnings.extend(row_warnings)
|
|
430
|
+
|
|
431
|
+
ok = len(flags) == 0
|
|
432
|
+
result = {"ok": ok, "flags": flags, "warnings": warnings}
|
|
433
|
+
json.dump(result, sys.stdout)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def main() -> None:
|
|
437
|
+
parser = argparse.ArgumentParser(
|
|
438
|
+
description="Validate Draft waiver recommendations against the waiver pool and squad.",
|
|
439
|
+
)
|
|
440
|
+
parser.add_argument(
|
|
441
|
+
"--recommendations-file",
|
|
442
|
+
required=True,
|
|
443
|
+
metavar="PATH",
|
|
444
|
+
dest="recommendations_file",
|
|
445
|
+
help="Path to gw{N}-recommendations.md.",
|
|
446
|
+
)
|
|
447
|
+
parser.add_argument(
|
|
448
|
+
"--waivers-json",
|
|
449
|
+
required=True,
|
|
450
|
+
metavar="PATH",
|
|
451
|
+
dest="waivers_json",
|
|
452
|
+
help="Path to fpl waivers --format json output.",
|
|
453
|
+
)
|
|
454
|
+
parser.add_argument(
|
|
455
|
+
"--squad-grid-json",
|
|
456
|
+
required=True,
|
|
457
|
+
metavar="PATH",
|
|
458
|
+
dest="squad_grid_json",
|
|
459
|
+
help="Path to fpl squad grid --format json output (draft squad).",
|
|
460
|
+
)
|
|
461
|
+
args = parser.parse_args()
|
|
462
|
+
_run(args.recommendations_file, args.waivers_json, args.squad_grid_json)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if __name__ == "__main__":
|
|
466
|
+
main()
|
|
@@ -18,6 +18,24 @@ You are an FPL mate - the kind who's run five mini-leagues, watches xG religious
|
|
|
18
18
|
- No emojis unless requested
|
|
19
19
|
- No time estimates
|
|
20
20
|
|
|
21
|
+
## Decision Heuristics
|
|
22
|
+
|
|
23
|
+
Never reason from a wrong rule. If you're unsure about a scoring or mechanics detail, say so and check `docs/fpl-rules.md` - don't guess.
|
|
24
|
+
|
|
25
|
+
- **Banking the FT is a live option.** FTs bank up to 5. Skipping a transfer buys flexibility next week, not wasted currency. Always weigh it against a marginal move.
|
|
26
|
+
- **Formation is a variable.** Valid: 3-4-3, 3-5-2, 4-3-3, 4-4-2, 4-5-1, 5-3-2, 5-4-1. Evaluate transfers against the 15-man squad, not a fixed XI - a same-position swap can mean "both play via reshape", not "one replaces the other". Compare all candidate sells at that position before committing.
|
|
27
|
+
- **Hit threshold: >8 pts expected gain** across the planning horizon before eating a -4.
|
|
28
|
+
- **Bench Boost = all 15 score once (not 2x).** Floor-raiser via bench minutes, not a multiplier.
|
|
29
|
+
- **Triple Captain = captain scores 3x (not 2x).**
|
|
30
|
+
- **Chip availability:** each chip twice per season, split at GW19. Check `fpl chips` before assuming.
|
|
31
|
+
|
|
32
|
+
## Draft Rules (load-bearing)
|
|
33
|
+
|
|
34
|
+
- **Waiver claims must be position-for-position.** Squad composition is fixed at 2 GK / 5 DEF / 5 MID / 3 FWD. Dropping a FWD means claiming a FWD, full stop. No cross-position swaps.
|
|
35
|
+
- **Waiver pool is only what `fpl waivers` returns.** Never recommend a player who isn't in that output, even if they look great in `fpl stats`. Premium names (Haaland, Salah, Palmer etc.) are almost always owned - check the waiver JSON, don't assume.
|
|
36
|
+
- **If no player is available at the position you need, say so.** Park the dead-weight asset and focus waiver priority elsewhere. Don't manufacture a bad cross-position claim to fill the slot.
|
|
37
|
+
- **Draft has no captain, no budget, no chips, no transfers.** Only waivers and trades.
|
|
38
|
+
|
|
21
39
|
## FPL Expertise
|
|
22
40
|
|
|
23
41
|
You have strong opinions and will argue them:
|
|
@@ -46,6 +46,9 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
|
|
|
46
46
|
### Exception Handling
|
|
47
47
|
- Never use bare `except Exception`. Use specific types for narrow try blocks; use `# noqa: BLE001 — <justification>` for intentional broad handlers (agent top-level, scraper resilience, graceful degradation)
|
|
48
48
|
|
|
49
|
+
### Timestamps
|
|
50
|
+
- User-facing timestamps (deadlines, kickoffs, `generated_at` stamps) must route through `fpl_cli/utils/time.py` (`format_deadline`, `format_kickoff`, `format_generated_at`, `now_uk`). Never `strftime` on a naive `datetime.now()` or print raw API ISO strings to users. Tool is UK-locked: display is always `Europe/London` with GMT/BST label. Internal datetime math stays UTC.
|
|
51
|
+
|
|
49
52
|
## FPL Domain Knowledge
|
|
50
53
|
- Chips (each available **twice** per season, split at GW19 deadline): Wildcard, Free Hit, Bench Boost, Triple Captain
|
|
51
54
|
- Scoring: GK/DEF clean sheet = 4pts, MID = 1pt; goals: DEF=6, MID=5, FWD=4; assist = 3pts; yellow = -1; red = -3
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [1.3.0] - 2026-04-17
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- correct stats key names and draft trailing heading (#19)
|
|
9
|
+
- inject explicit chip roster into synthesis prompt
|
|
10
|
+
- six LLM/formatting fixes for gw review output (#26)
|
|
11
|
+
- use custom team-ratings FDR in GW fixtures table when custom_analysis enabled
|
|
12
|
+
- drop understat position from enrichment spread
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- embed squad-builder output on wildcard/freehit weeks (#23)
|
|
17
|
+
- surface active chips in recap narrative
|
|
18
|
+
- detect bench boost chip and adjust analysis emphasis
|
|
19
|
+
- chip-aware analysis for WC, FH, BB, and TC (#24)
|
|
20
|
+
- WhatsApp-friendly standings text block (#25)
|
|
21
|
+
|
|
22
|
+
### Refactoring
|
|
23
|
+
|
|
24
|
+
- consolidate ownership ceiling dispatch into single helper
|
|
25
|
+
- thread Position literal through scoring pipeline (#21)
|
|
26
|
+
- rename horizon=1 output field to single_gw_score (#22)
|
|
27
|
+
- tighten project SKILL.md prompt contract and phase ordering
|
|
28
|
+
|
|
4
29
|
## [1.2.1] - 2026-04-10
|
|
5
30
|
|
|
6
31
|
### Bug Fixes
|
|
@@ -46,6 +46,9 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
|
|
|
46
46
|
### Exception Handling
|
|
47
47
|
- Never use bare `except Exception`. Use specific types for narrow try blocks; use `# noqa: BLE001 — <justification>` for intentional broad handlers (agent top-level, scraper resilience, graceful degradation)
|
|
48
48
|
|
|
49
|
+
### Timestamps
|
|
50
|
+
- User-facing timestamps (deadlines, kickoffs, `generated_at` stamps) must route through `fpl_cli/utils/time.py` (`format_deadline`, `format_kickoff`, `format_generated_at`, `now_uk`). Never `strftime` on a naive `datetime.now()` or print raw API ISO strings to users. Tool is UK-locked: display is always `Europe/London` with GMT/BST label. Internal datetime math stays UTC.
|
|
51
|
+
|
|
49
52
|
## FPL Domain Knowledge
|
|
50
53
|
- Chips (each available **twice** per season, split at GW19 deadline): Wildcard, Free Hit, Bench Boost, Triple Captain
|
|
51
54
|
- Scoring: GK/DEF clean sheet = 4pts, MID = 1pt; goals: DEF=6, MID=5, FWD=4; assist = 3pts; yellow = -1; red = -3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: CLI tool for Fantasy Premier League analysis - classic and draft formats
|
|
5
5
|
Project-URL: Homepage, https://github.com/rossgroomio/fpl-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/rossgroomio/fpl-cli
|
|
@@ -19,6 +19,7 @@ Requires-Dist: python-dotenv>=1.0.0
|
|
|
19
19
|
Requires-Dist: pyyaml>=6.0.0
|
|
20
20
|
Requires-Dist: rich>=13.7.0
|
|
21
21
|
Requires-Dist: ruamel-yaml>=0.18.0
|
|
22
|
+
Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
|
|
22
23
|
Provides-Extra: dev
|
|
23
24
|
Requires-Dist: pyright>=1.1.0; extra == 'dev'
|
|
24
25
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
@@ -551,7 +551,10 @@ fpl_cli/
|
|
|
551
551
|
│ └── fpl_prices.py # FPLPriceScraper (needs FPL_EMAIL/FPL_PASSWORD)
|
|
552
552
|
├── paths.py # SHIPPED_CONFIG_DIR, TEMPLATE_DIR, user_config_dir(), user_data_dir()
|
|
553
553
|
├── season.py # season_label() (+ vaastav_season() alias), TOTAL_GAMEWEEKS, CHIP_SPLIT_GW
|
|
554
|
-
|
|
554
|
+
├── constants.py # MIN_MINUTES_FOR_PER90
|
|
555
|
+
└── utils/
|
|
556
|
+
├── text.py # strip_diacritics (name matching across sources)
|
|
557
|
+
└── time.py # format_deadline/format_kickoff/format_generated_at — UK local (Europe/London, auto GMT↔BST). Canonical formatter for every user-facing timestamp.
|
|
555
558
|
|
|
556
559
|
platformdirs (user_config_dir / user_data_dir) # macOS: ~/Library/Application Support/fpl-cli/
|
|
557
560
|
├── settings.yaml # User overrides, created by `fpl init`
|