fplkit 2.7.1__tar.gz → 2.7.2__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-2.7.1 → fplkit-2.7.2}/.agents/TOOLS.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/CHANGELOG.md +8 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/PKG-INFO +6 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/README.md +5 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/docs/architecture.md +2 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/docs/command-reference.md +29 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/docs/custom-analysis.md +1 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/_version.py +2 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/orchestration/report.py +8 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_data.py +149 -15
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_types.py +28 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/allocate.py +2 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/league_recap.py +9 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/prompts/league_recap.py +102 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/constants.py +7 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/single_gw.py +1 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/templates/gw_league_recap.md.j2 +2 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/markdown.py +25 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_league_recap.py +75 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_league_recap.py +562 -18
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_preseason.py +6 -1
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_scoring.py +5 -2
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_utils_markdown.py +34 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/README.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.claude/settings.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.env.example +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.github/workflows/ci.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.github/workflows/release.yml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/.gitignore +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/AGENTS.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/CLAUDE.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/CONTRIBUTING.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/LICENSE +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/cliff.toml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/docs/fpl-rules.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_context.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_helpers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_league_recap_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_review_classic.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_review_draft.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/_review_summarisation.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/preview.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/constants.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/team.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/models/types.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/prompts/review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/season.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/league_history_notes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/templates/gw_review.md.j2 +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/text.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/pyproject.toml +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/pyrightconfig.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/__init__.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/conftest.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_action.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_base.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_allocate.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_client.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_draft.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_football_data.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_historical.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_understat.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_banner.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_captain.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_chips.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_doctor.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_format.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_intel.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_json.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_league.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_player.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_preview.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_squad.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_status.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_targets.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_cli_xg.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_common.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_common_scoring.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_dependencies.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_fines_config.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_init.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_init_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_league_history_notes.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_league_history_store.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_matchup_service.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_models.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_player_clubs.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_player_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_player_resolution.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_providers.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_report_agent.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_report_paths.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_review.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_scraper.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_season.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_season_awareness.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_season_previews.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_sell_prices.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_stats.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_team_ratings.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_utils_text.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_utils_time.py +0 -0
- {fplkit-2.7.1 → fplkit-2.7.2}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -47,7 +47,7 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
47
47
|
| `returnees` | Track injured and suspended players due back soon, so a returnee can be claimed in draft before they are fit or planned around in classic. Reads the FPL availability news, filters to returns inside a window (`--window N`, 1-38) and through a source-aware quality bar, and diffs against the stored watchlist for week-over-week changes. Most flagged players carry no parseable return date, so date-unknown entries are the norm rather than the exception; `--enrich` searches the web for fresher timing on those and on stale-news entries (needs a Perplexity key; skips with a note without one) and shows it beside the FPL news, never over it. `--all` bypasses the quality bar and deliberately does not persist the snapshot. JSON `metadata` carries `window`, `escalation_window`, `stash_upgrade_margin`, `transitions_available`, `transitions_baseline_gameweek` (the gameweek `transition` is measured against — every run in a gameweek diffs against the last state stored before it, so a repeat run reports the same changes), `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`/`_rate_limited` (true when the search provider rate-limited the run even after retrying — the one failure a re-run is likely to clear); `data` is `{entries, departures}`, each entry carrying `quality.basis`/`quality.meets_stash`, `expected_return`/`return_gameweek`/`return_source`, `escalation_eligible`/`escalation_basis`, `transition`, and `enrichment` (null unless enriched) | Yes | General | No | direct-api |
|
|
48
48
|
| `preview` | Run full pre-gameweek analysis and generate report. `--save` writes to `<output dir>/<season>/gw{N}-preview.md`; `--scout` writes to `<research dir>/ai-scout-reports/<season>/gw{N}-scout-preview[-referenced].md` | No | General | No | via-agent |
|
|
49
49
|
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider says it stopped early on is retried once, and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
|
|
50
|
-
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`. Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider stopped early on is written out as it stands with the reason on stderr and a `synthesis_stopped_early` warning in JSON. A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
50
|
+
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider stopped early on is written out as it stands with the reason on stderr and a `synthesis_stopped_early` warning in JSON. A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
51
51
|
| `league-fines` | Show who has been fined this season and how much of it was actually ruled, folded out of the league history ledger. Reads local files only - no network - so it works for any season still on disk (`--season 2025-26`). Nothing is re-ruled: a fine is counted exactly as its gameweek recorded it — including one `league-recap` appended against a manager who tied for last and was left out at capture. Counts, never money (`penalty` is free text; settlement and escalation are not modelled). Every gameweek that could not be ruled is named beneath the table - never captured, unreadable, captured before rulings were recorded, captured with no rules configured, or captured at a fidelity that could not rule a given rule. `--format json` emits one entry per recorded manager, fined or not, each with `counts` (zero included), `total`, `fined_gameweeks`, `ruled_gameweeks`, `unruled_gameweeks`, `first_recorded_gameweek`, `last_recorded_gameweek` and `is_fully_ruled`; `metadata` carries `season`, `fpl_format`, `league_id`, `gameweek`, `start_gameweek`, `rule_types`, `total_fines` and `qualifiers`. Exits 1 with the shared error envelope when no league id is configured for the format | Yes | General | No | direct-api |
|
|
52
52
|
| `captain` | Analyse and rank captain options for next gameweek. Your squad is the default, so an absent `classic_entry_id` exits 1 with an `error` envelope naming `--global` rather than returning the global list as though it were your options; `metadata.my_squad_mode` says which list the payload holds, and `metadata.warnings` carries `captain_global_fallback` when there is no squad to rank yet (before the first deadline) or `captain_no_next_gameweek` when there is no next gameweek at all (finished season, or the next one unpublished). An entry ID that does not resolve is a failure, not a fallback, worded exactly as `squad` words it (`get_own_squad_picks`, `agents/common.py`). JSON candidates include `adjusted_npxg_per_90` and `raw_npxg_per_90` when fixture adjustment is active. Consistency tiebreaker (CV-xGI percentile) phased in GW6-10. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the GW1-9 shrinkage rather than pulled toward the position mean | Yes | Classic | Yes | via-agent |
|
|
53
53
|
| `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the ownership, matchup and consistency bonuses stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. The gameweek stamped into JSON `metadata` is fetched after the agent succeeds, not before it runs, so an unreachable API is reported as the agent's failure in both output formats rather than as a client error under `--format json` and an agent error in table mode | Yes | Classic | Yes | via-agent |
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.7.1] - 2026-09-09
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- stop the review mismatching names over spacing or apostrophes (#346)
|
|
9
|
+
- give managers level on points the same position in `fpl league` (#345)
|
|
10
|
+
- stop a draft waiver a manager won reading as one he lost (#347)
|
|
11
|
+
|
|
4
12
|
## [2.7.0] - 2026-09-08
|
|
5
13
|
|
|
6
14
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
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
|
|
@@ -116,6 +116,11 @@ so `review --summarise` with a key for one provider prints that half, says on st
|
|
|
116
116
|
why the other was skipped, and still exits 0; a saved report names each skipped half
|
|
117
117
|
in its own warning callout. `league-recap --summarise` does the same for its editorial.
|
|
118
118
|
|
|
119
|
+
A saved recap opens with a fixed title — `# Gameweek 4 Recap: <league name>`, the league's name
|
|
120
|
+
as configured — whether or not it has an editorial. The editorial's own headline sits beneath it
|
|
121
|
+
as a `##` subheading, never as the title, so the first line you paste into the group chat reads
|
|
122
|
+
the same every week and the name on it is the league's rather than one the model restyled.
|
|
123
|
+
|
|
119
124
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
120
125
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
121
126
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
@@ -81,6 +81,11 @@ so `review --summarise` with a key for one provider prints that half, says on st
|
|
|
81
81
|
why the other was skipped, and still exits 0; a saved report names each skipped half
|
|
82
82
|
in its own warning callout. `league-recap --summarise` does the same for its editorial.
|
|
83
83
|
|
|
84
|
+
A saved recap opens with a fixed title — `# Gameweek 4 Recap: <league name>`, the league's name
|
|
85
|
+
as configured — whether or not it has an editorial. The editorial's own headline sits beneath it
|
|
86
|
+
as a `##` subheading, never as the title, so the first line you paste into the group chat reads
|
|
87
|
+
the same every week and the name on it is the league's rather than one the model restyled.
|
|
88
|
+
|
|
84
89
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
85
90
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
86
91
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
@@ -531,7 +531,7 @@ fpl_cli/
|
|
|
531
531
|
│ ├── _banner.py # Startup banner
|
|
532
532
|
│ ├── _plan_grid.py # Fixture grid rendering
|
|
533
533
|
│ ├── _review_*.py # Review command helpers (analysis, classic, draft, summarisation)
|
|
534
|
-
│ ├── _league_recap_*.py # League recap helpers & types (`_league_recap_history.py` orchestrates capture: builds ledger rows, corrects previous league position from recorded rows, runs the two-tier backfill including the coarse tier's partial fine ruling, keeps the name/club/position a replayed gameweek already recorded rather than restamping it from today's bootstrap (`_carry_recorded_identity`, reading the earliest recorded line rather than the resolved winner so an already-restamped gameweek is repaired — a lost waiver claim's two players carried the same way, off the earliest line that recorded any claims), restates the players a stored fine names from that corrected squad, after the freeze so a ruling already written wrong is repaired rather than frozen wrong forever (`_repair_fine_identity`), keeps the league position and cumulative total the same way (`_carry_recorded_standings`, but reading the *last* recorded value rather than the earliest -- a table settles over a gameweek where a name does not) and reconstructs a draft gameweek's from the ledger where it can (`_fill_draft_standings`), sweeps already-damaged gameweeks back into shape without re-fetching anything (`_repair_recorded_standings`, and `_repair_tied_last_place` for a gameweek that fined one manager of a tie for last), and returns the notes pack and season fine tally the console, report, prompt and JSON payload all read; `_league_recap_data.py` also holds `fetch_manager_histories()`, the one manager-history fetch that `collect_classic_recap_data(with_prior_seasons=True)` makes at the league's opener and shares between the league-start offset and `collect_prior_seasons()` — asked for only when a report or editorial will read it, never by a replay, and never a ledger field — and `league_first_gameweek()`, the one definition of a league's first scored gameweek that both collectors' gates and this module's coverage, backfill, notes pack and fines tally read; `_league_recap_types.py` holds the TypedDicts and the pure helpers both the data module and the prompt builder read — the draft kind labels, `summarise_prior_seasons()`, and `contested_draft_claims()` / `format_contested_claim()`, which regroup a draft gameweek's lost claims by player, read the winner off the accepted move that brought him in, and print one sentence per race for the Most Contested award and the editorial alike (issue #330) — the award gated on `MOST_CONTESTED_MIN_CLAIMANTS` in `_league_recap_data.py`, the editorial handed every race)
|
|
534
|
+
│ ├── _league_recap_*.py # League recap helpers & types (`_league_recap_history.py` orchestrates capture: builds ledger rows, corrects previous league position from recorded rows, runs the two-tier backfill including the coarse tier's partial fine ruling, keeps the name/club/position a replayed gameweek already recorded rather than restamping it from today's bootstrap (`_carry_recorded_identity`, reading the earliest recorded line rather than the resolved winner so an already-restamped gameweek is repaired — a lost waiver claim's two players carried the same way, off the earliest line that recorded any claims), restates the players a stored fine names from that corrected squad, after the freeze so a ruling already written wrong is repaired rather than frozen wrong forever (`_repair_fine_identity`), keeps the league position and cumulative total the same way (`_carry_recorded_standings`, but reading the *last* recorded value rather than the earliest -- a table settles over a gameweek where a name does not) and reconstructs a draft gameweek's from the ledger where it can (`_fill_draft_standings`), sweeps already-damaged gameweeks back into shape without re-fetching anything (`_repair_recorded_standings`, and `_repair_tied_last_place` for a gameweek that fined one manager of a tie for last), and returns the notes pack and season fine tally the console, report, prompt and JSON payload all read; `_league_recap_data.py` also holds `fetch_manager_histories()`, the one manager-history fetch that `collect_classic_recap_data(with_prior_seasons=True)` makes at the league's opener and shares between the league-start offset and `collect_prior_seasons()` — asked for only when a report or editorial will read it, never by a replay, and never a ledger field — and `league_first_gameweek()`, the one definition of a league's first scored gameweek that both collectors' gates and this module's coverage, backfill, notes pack and fines tally read; `_league_recap_types.py` holds the TypedDicts and the pure helpers both the data module and the prompt builder read — the draft kind labels, `summarise_prior_seasons()`, and `contested_draft_claims()` / `format_contested_claim()`, which regroup a draft gameweek's lost claims by player, read the winner off the accepted move that brought him in, and print one sentence per race for the Most Contested award and the editorial alike (issue #330) — the award gated on `MOST_CONTESTED_MIN_CLAIMANTS` in `_league_recap_data.py`, the editorial handed every race; `_league_recap_data.py` also holds `best_xi_selection()`, the pure reselection of the best legal XI from a recorded 15 that both collectors stamp on the entry as `best_xi_gain` and the Biggest Bench award ranks on — one keeper, the eight `VALID_FORMATIONS`, the doubled player held in place, ties to the XI that played — where the bench's raw sum, kept as `bench_points` for the ledger's sake, overstated the award every week (issue #350))
|
|
535
535
|
│ ├── _fines.py / _fines_config.py # League fines system, including the cohort-only/needs-a-squad rule split the coarse ledger tier narrows by
|
|
536
536
|
│ ├── league_fines.py # `league-fines`: season fine table read straight off the ledger, no network
|
|
537
537
|
│ ├── doctor_providers.py # Live provider probes for `doctor --providers` (shape/volume/join checks; the Core-Insights per-GW probe runs the real parsers, so it cannot pass a file the runtime reads as zero records, and the Understat name-join probe runs the real matcher, so a club resolving cannot stand in for its players joining)
|
|
@@ -596,7 +596,7 @@ fpl_cli/
|
|
|
596
596
|
│ ├── scout.py # ScoutAgent system/user prompts
|
|
597
597
|
│ ├── review.py # Review research + synthesis prompts. Squad and transfer lines in the synthesis data block carry each player's full club name, and the hard constraints bind club affiliation and blanket scored/blanked claims to that supplied data — an LLM's own club knowledge goes stale every transfer window. The synthesis prompt also carries a `<next_gameweek>` block — every club's next fixture and each squad player's positional FDR, built by `_review_next_gameweek` / `_format_next_gameweek` — and the "Next Week" instruction switches on whether it is there: grounded advice when it is, fixture-independent observations pointing at `fpl gw-prep` when it is not, because a results-only review was issuing confident fixture-dependent calls with no fixture data behind them (#191), with `check_next_week_grounding` checking the answer back against that block -- a cited FDR figure the block never printed, or any fixture claim on a run that had no block -- so the grounding claim is not left resting on prompt prose alone, the way the research half's rules are backed by `validate_research_teams`, which folds both the supplied name and the model's cell through `normalise_name` before the allowlist search so a reformat is not read as an invention, and leaves a `[table trimmed: N unlisted row(s) removed]` line under a table it did shorten — the strip notice otherwise reaching stderr and `--debug` only, both gone by the time the markdown is read (#343). Also the synthesis half's post-generation guard (`check_synthesis_completeness`, `required_synthesis_sections`), which reads the required `## ` headings back off the prompt that was actually sent rather than restating them as a constant. The research half's counts are grounded the same way: `fixture_aggregates()` computes the fixture, goal, clean-sheet and goalless-draw totals once, `FixtureAggregates.summary_line()` pins them at the top of the GW Results block as the narrative's only permitted source, and `validate_research_counts` checks the narrative back against the same object — a count the prompt pins cannot drift from the count the check enforces. It rewrites a contradicting figure in place rather than scrubbing the sentence, and corrects one only when neither its sentence nor the one before it names anyone the count could belong to — a club, a nickname, a player, a pronoun standing in for one — because a round-wide total is exactly the sentence that names nobody. The guard errs towards leaving prose alone: a missed correction ships the model's own words, while a wrong one fabricates a claim in prose that was true. That blind spot is why the check was needed at all — the narrative that reported six clean sheets as four (#324) named no player, so all three name-based validators passed it
|
|
598
598
|
│ ├── returnees.py # Return-intel search prompt for `fpl returnees --enrich` (one player per query, so a citation list belongs to a single player)
|
|
599
|
-
│ └── league_recap.py # League recap synthesis prompts, including the anchored League History section, its never-infer-history rule (emitted even when the pack is empty), and the season-phase framing instruction. The GW Standings section leads with an explicit "Previous gameweek's leader" fact (issue #189) so a "topped the table"/"fell from the top" claim is checkable against a stated name instead of inferred from the Prev column or the size of a fall, and the rules forbid merging two managers' separate League History entries into a shared record or "club". `collect_player_clubs()` regroups the club each squad/transfer row already resolved off its `team_id` by the name the prose uses, feeding both a `## Player Clubs` roster and the club printed inline on each captain group, so club claims bind to supplied data instead of recall; a name the data gives two clubs for is dropped rather than guessed at, since the prose can't tell the two players apart. Transfers get the same full-roster treatment as captains and chips (issue #71): `format_recap_transfers_context()` lists every manager who transferred with each move, the hit and the post-hit net, names who made none, and keeps a manager whose moves the collector could not capture among the movers with the moves marked unknown, so the rule pins transfer narration to that section rather than to the two award winners — the only transfer data the model used to see, which it read as the whole story. `format_recap_waivers_context()` is the draft half (issue #301): every waiver and free-agent move as it was made, never chain-contracted (that is the awards' display compression, not a record of activity), each tagged by kind through the shared `draft_transaction_kind_label()` / `draft_transaction_kind_counts()` in `_league_recap_types.py` so the roster and the waiver awards never describe one move in two vocabularies; the rule names that section as draft's source of truth and only falls back to "say nothing about moves" when neither roster is present. It states three groups rather than two (issue #329): movers, the managers who claimed a player and lost him to a rival, and the managers who did neither. A waiver is a competition, so an outbid manager ends the gameweek with no accepted move and used to be indistinguishable from one who submitted nothing -- the editorial read that absence as inactivity and assigned a motive for it, so the rule now forbids calling anyone in the outbid group idle. The section closes with the gameweek's contested players (issue #330) — one sentence per player more than one manager claimed, from `contested_draft_claims()` / `format_contested_claim()` in `_league_recap_types.py`, the same sentence the Most Contested award prints for a pile-up of `MOST_CONTESTED_MIN_CLAIMANTS` or more that a recapped manager won (a race nobody can be seen to have won is context, never the award), with a rule pinning who-wanted-whom to those lines and limiting what may be said of anyone's waiver order to the winner having stood higher in it. `format_recap_prior_seasons_context()` (issue #131) is the opener's one source of history: each classic manager's FPL-wide seasons before this one, folded once by `summarise_prior_seasons()` (`_league_recap_types.py`) so the saved report and the prompt read the same lines, with an explicit count, the no-record and unfetched managers named rather than omitted, and a rule pinning tenure to FPL-wide phrasing — entry and league IDs are reissued each season, so "Nth season in this league" is unknowable and the section says so
|
|
599
|
+
│ └── league_recap.py # League recap synthesis prompts, including the anchored League History section, its never-infer-history rule (emitted even when the pack is empty), and the season-phase framing instruction. The GW Standings section leads with an explicit "Previous gameweek's leader" fact (issue #189) so a "topped the table"/"fell from the top" claim is checkable against a stated name instead of inferred from the Prev column or the size of a fall, and the rules forbid merging two managers' separate League History entries into a shared record or "club". `collect_player_clubs()` regroups the club each squad/transfer row already resolved off its `team_id` by the name the prose uses, feeding both a `## Player Clubs` roster and the club printed inline on each captain group, so club claims bind to supplied data instead of recall; a name the data gives two clubs for is dropped rather than guessed at, since the prose can't tell the two players apart. Transfers get the same full-roster treatment as captains and chips (issue #71): `format_recap_transfers_context()` lists every manager who transferred with each move, the hit and the post-hit net, names who made none, and keeps a manager whose moves the collector could not capture among the movers with the moves marked unknown, so the rule pins transfer narration to that section rather than to the two award winners — the only transfer data the model used to see, which it read as the whole story. `format_recap_waivers_context()` is the draft half (issue #301): every waiver and free-agent move as it was made, never chain-contracted (that is the awards' display compression, not a record of activity), each tagged by kind through the shared `draft_transaction_kind_label()` / `draft_transaction_kind_counts()` in `_league_recap_types.py` so the roster and the waiver awards never describe one move in two vocabularies; the rule names that section as draft's source of truth and only falls back to "say nothing about moves" when neither roster is present. It states three groups rather than two (issue #329): movers, the managers who claimed a player and lost him to a rival, and the managers who did neither. A waiver is a competition, so an outbid manager ends the gameweek with no accepted move and used to be indistinguishable from one who submitted nothing -- the editorial read that absence as inactivity and assigned a motive for it, so the rule now forbids calling anyone in the outbid group idle. The section closes with the gameweek's contested players (issue #330) — one sentence per player more than one manager claimed, from `contested_draft_claims()` / `format_contested_claim()` in `_league_recap_types.py`, the same sentence the Most Contested award prints for a pile-up of `MOST_CONTESTED_MIN_CLAIMANTS` or more that a recapped manager won (a race nobody can be seen to have won is context, never the award), with a rule pinning who-wanted-whom to those lines and limiting what may be said of anyone's waiver order to the winner having stood higher in it. `format_recap_prior_seasons_context()` (issue #131) is the opener's one source of history: each classic manager's FPL-wide seasons before this one, folded once by `summarise_prior_seasons()` (`_league_recap_types.py`) so the saved report and the prompt read the same lines, with an explicit count, the no-record and unfetched managers named rather than omitted, and a rule pinning tenure to FPL-wide phrasing — entry and league IDs are reissued each season, so "Nth season in this league" is unknowable and the section says so. The report's title is not the model's (issue #349): `recap_title()` (`_league_recap_types.py`) is the one H1, read by the template that renders it and the prompt that names it as already written; the prompt asks for a `## ` headline beneath it and no title, and `normalise_recap_editorial()` holds the reply to that shape in one fence-aware pass over every heading — one that only restates the title is dropped wherever it sits, the opening one is kept at H2 with the title's parts taken off (emphasis unwrapped through `unwrap_emphasis()` in `utils/markdown.py`, balanced pairs only, so what is printed again stays well-formed), a later `# ` is demoted, and a title-shaped part is removed only behind a colon, pipe or spaced dash so a hook's own words are never edited — since an instruction is not a contract and the model had mirrored, restyled, renamed and skipped the heading in turn
|
|
600
600
|
├── parsers/
|
|
601
601
|
│ └── recommendations.py # Parse gw{N}-recommendations.md into structured decisions
|
|
602
602
|
├── scraper/
|
|
@@ -292,7 +292,7 @@ fpl allocate --format json # JSON output for scripting / skill integrat
|
|
|
292
292
|
| `--sell-prices` | - | Path to sell-prices JSON file (from `fpl squad sell-prices --format json`). Solver uses sell prices for owned players in budget constraint. Budget auto-computed as `sum(sell_prices) + bank` unless `--budget` is explicitly set |
|
|
293
293
|
| `--format` | table | `table` or `json` |
|
|
294
294
|
|
|
295
|
-
Scores ~500 eligible players, adjusts for fixture difficulty over the planning horizon, then solves for the budget-constrained optimum across all
|
|
295
|
+
Scores ~500 eligible players, adjusts for fixture difficulty over the planning horizon, then solves for the budget-constrained optimum across all 8 valid formations. See [Squad Allocator](custom-analysis.md#squad-allocator) for scoring methodology, fixture coefficients, and solver detail.
|
|
296
296
|
|
|
297
297
|
**JSON output fields (horizon >= 2):** `id`, `web_name`, `team`, `position`, `price`, `quality_score` (0-100), `raw_quality` (float), `role` (starter/bench), `captain_gws`. Metadata includes `formation`, `budget_used`, `budget_remaining`, `captain_schedule`, `solver_status`, and `warnings` — the early-season quality notice before GW10 (`early_season_prior_informed`, or `early_season_small_sample` when last season's history could not be loaded and the solver ranked on pure observation), empty otherwise and at horizon 1; table mode prints it to stderr.
|
|
298
298
|
|
|
@@ -853,7 +853,33 @@ a `league_standings_moved_on` warning under `--format json`. The gameweek is rec
|
|
|
853
853
|
either way — an in-progress gameweek is never recappable, so the recap of the one before it
|
|
854
854
|
has to keep working while the next one plays.
|
|
855
855
|
|
|
856
|
+
**Report title:** a saved recap opens with `# Gameweek 4 Recap: <league name>` — the gameweek
|
|
857
|
+
spelled out and the league's name exactly as configured — whether or not it carries an editorial,
|
|
858
|
+
so the first line of every recap reads the same shape every week and in both formats
|
|
859
|
+
([#349](https://github.com/rossgroomio/fpl-cli/issues/349)). The title is written by the report,
|
|
860
|
+
never by the model: with `--summarise` the editorial is asked for one `##` headline beneath it — a
|
|
861
|
+
hook for the gameweek, never the gameweek number, the word "recap" or the league's name — and
|
|
862
|
+
every heading it writes anyway is held to that shape rather than trusted, wherever it sits. One
|
|
863
|
+
that only restates the title (`# GW4 Recap: <league name>`, in any order, spelling or emphasis)
|
|
864
|
+
is dropped, the opening one is kept as the `##` headline with the title's parts removed, a later
|
|
865
|
+
`#` heading is demoted, and a fenced code block is left alone. Only a title-shaped part joined by
|
|
866
|
+
a colon, pipe or spaced dash is removed — a gameweek callback inside a hook ("Bob Never Learns -
|
|
867
|
+
Gameweek 7") stays. The metadata lines sit between the title and the headline, so the editorial —
|
|
868
|
+
headline and prose — is one contiguous block to copy out.
|
|
869
|
+
|
|
856
870
|
**Awards:** GW winner/loser, biggest bench haul, best/worst captain, transfer/waiver genius and disaster.
|
|
871
|
+
Biggest Bench Haul ranks on what the bench actually cost: how many more points the best legal XI
|
|
872
|
+
from the same 15 would have scored than the XI that counted, with the player whose points were
|
|
873
|
+
doubled (the captain, or the vice when the captain did not play) held in place so the multiplier is
|
|
874
|
+
never quietly reassigned — not on the bench's raw points sum, which counts a keeper who could not
|
|
875
|
+
have played alongside the one who did and outfielders no legal formation had room for, and so
|
|
876
|
+
overstated every week and could hand the award to the wrong manager (issue #350). A manager with
|
|
877
|
+
points on the bench but no better XI is not a candidate, and the detail names the swap rather than
|
|
878
|
+
the bench ("Groß (17) and Gvardiol (11) benched while Rice (3) and Colwill (2) played"). Ties go
|
|
879
|
+
the way of the XI that played: a bench player level with a starter is never named, and of two
|
|
880
|
+
formations level on points the one that changes fewer players is the one described. The ledger row
|
|
881
|
+
and `--format json` keep `bench_points` as the raw sum — the API's own `points_on_bench`, the one
|
|
882
|
+
figure the coarse tier can also record — and the gain is re-derived from `squad` rather than stored.
|
|
857
883
|
The draft waiver awards count and label waiver claims and free-agent signings separately (e.g. "across
|
|
858
884
|
2 moves: 1 waiver, 1 free agent") rather than folding both into "waivers". A draft recap has a third
|
|
859
885
|
waiver award, **Most Contested**, on a week where at least three managers claimed the same player:
|
|
@@ -970,7 +996,8 @@ gameweek with a file on disk appears there with a status, a damaged one as
|
|
|
970
996
|
(every entry, including those below their reporting minimum and every nonzero season
|
|
971
997
|
count whether or not it grew this gameweek), `season_fines` (the whole
|
|
972
998
|
season tally, emitted every week regardless of the milestone gate the printed surfaces use), `synthesis_summary` (with
|
|
973
|
-
`--summarise
|
|
999
|
+
`--summarise`, as it reaches the saved report — the model's heading already demoted beneath the
|
|
1000
|
+
report's title or dropped, see [Report title](#league-recap)), `warnings`, and `first_capture_store_path` — always present, carrying the
|
|
974
1001
|
partition directory on its first capture and `null` on every run after that. Warning
|
|
975
1002
|
codes are listed under [Capture warnings](#capture-warnings).
|
|
976
1003
|
|
|
@@ -218,7 +218,7 @@ For horizon >= 2, per-player, per-GW fixture coefficients use position-variant s
|
|
|
218
218
|
|
|
219
219
|
### ILP Solver
|
|
220
220
|
|
|
221
|
-
Solves
|
|
221
|
+
Solves 8 independent ILPs (one per valid formation). The objective function maximises the weighted sum of expected contributions across all players and gameweeks:
|
|
222
222
|
|
|
223
223
|
```
|
|
224
224
|
max Σ(gw) Σ(p) discount[gw] × coeff[p][gw] × (starter[p] + bench_discount[p] × bench[p])
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 7,
|
|
21
|
+
__version__ = version = '2.7.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 7, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,7 +11,11 @@ import jinja2
|
|
|
11
11
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
12
12
|
|
|
13
13
|
from fpl_cli.agents.base import Agent, AgentResult, AgentStatus
|
|
14
|
-
from fpl_cli.cli._league_recap_types import
|
|
14
|
+
from fpl_cli.cli._league_recap_types import (
|
|
15
|
+
RecapManagerEntry,
|
|
16
|
+
draft_transaction_kind_label,
|
|
17
|
+
recap_title,
|
|
18
|
+
)
|
|
15
19
|
from fpl_cli.paths import TEMPLATE_DIR
|
|
16
20
|
from fpl_cli.services.team_ratings import fdr_columns_footer
|
|
17
21
|
from fpl_cli.utils.text import ordinal_suffix
|
|
@@ -657,6 +661,9 @@ class ReportAgent(Agent):
|
|
|
657
661
|
def _generate_league_recap_report(self, gameweek: int, data: dict[str, Any]) -> str:
|
|
658
662
|
"""Generate a league recap report."""
|
|
659
663
|
template = self.jinja_env.get_template("gw_league_recap.md.j2")
|
|
664
|
+
# The H1 is the report's, never the editorial's (#349): the same
|
|
665
|
+
# string the prompt names as already written, so the two agree.
|
|
666
|
+
data["title"] = recap_title(gameweek, data.get("league_name", ""))
|
|
660
667
|
data.setdefault("generated_at", format_generated_at())
|
|
661
668
|
data["standings_block"] = _format_standings_block(data.get("managers", []))
|
|
662
669
|
return template.render(**data)
|
|
@@ -73,6 +73,7 @@ from fpl_cli.cli._league_recap_types import (
|
|
|
73
73
|
)
|
|
74
74
|
from fpl_cli.services.fixture_predictions import had_fixture
|
|
75
75
|
from fpl_cli.services.player_clubs import gameweek_club
|
|
76
|
+
from fpl_cli.services.scoring.constants import VALID_FORMATIONS
|
|
76
77
|
|
|
77
78
|
logger = logging.getLogger(__name__)
|
|
78
79
|
|
|
@@ -159,6 +160,122 @@ def _classic_pick_flags(
|
|
|
159
160
|
return is_bench, is_bench_boost_player, contributed
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class BestXISelection:
|
|
165
|
+
"""The best legal XI from a recorded 15, set against the XI that counted.
|
|
166
|
+
|
|
167
|
+
`gain` is how many more points it would have scored -- what the Biggest
|
|
168
|
+
Bench award ranks on. `benched` are the players it brings in off the
|
|
169
|
+
bench and `dropped` the ones it leaves out of the XI that counted: the
|
|
170
|
+
two halves of one swap, each highest scorer first. Both are empty, and
|
|
171
|
+
the gain 0, when no legal XI beats the one that played -- or when the
|
|
172
|
+
recorded squad cannot field one at all (a pick the bootstrap could not
|
|
173
|
+
resolve leaves it a player short).
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
gain: int
|
|
177
|
+
benched: tuple[RecapManagerPlayer, ...]
|
|
178
|
+
dropped: tuple[RecapManagerPlayer, ...]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
_NO_BETTER_XI = BestXISelection(gain=0, benched=(), dropped=())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def best_xi_selection(
|
|
185
|
+
squad: Sequence[RecapManagerPlayer],
|
|
186
|
+
*,
|
|
187
|
+
captain_played: bool,
|
|
188
|
+
) -> BestXISelection:
|
|
189
|
+
"""Reselect the best legal XI from a manager's 15 on the points they scored.
|
|
190
|
+
|
|
191
|
+
The bench's raw sum overstates what the bench cost every time (issue
|
|
192
|
+
#350): only one goalkeeper plays, so a bench keeper's points are worth
|
|
193
|
+
the difference against the one who did and nothing when he scored less;
|
|
194
|
+
and an outfielder can only come in for someone one of the eight legal
|
|
195
|
+
formations lets him replace. For each formation this takes the top
|
|
196
|
+
scorers by position and keeps the best total. Its gain over the XI that
|
|
197
|
+
counted -- the players who `contributed`, auto-subs applied -- is exactly
|
|
198
|
+
the points left on the bench, and nothing else is.
|
|
199
|
+
|
|
200
|
+
The player whose points were doubled stays in the XI: the captain when
|
|
201
|
+
he played, the vice-captain when he did not (and only where the vice
|
|
202
|
+
counted himself -- a vice auto-subbed out doubled nobody). Left free,
|
|
203
|
+
the reselection would quietly move the armband and count a multiplier
|
|
204
|
+
nobody could have had. Draft has no captain, so nothing is held.
|
|
205
|
+
|
|
206
|
+
Ties go the way of the XI that played: a bench player level with a
|
|
207
|
+
starter is never named as wrongly benched, a formation that only matches
|
|
208
|
+
the actual total is not a better one, and of two formations level on the
|
|
209
|
+
best total the one that changes fewer players is the one named -- so
|
|
210
|
+
nobody is named as benched for a swap that gained nothing. Bench Boost
|
|
211
|
+
has all 15 counting, so the gain is 0 by construction.
|
|
212
|
+
|
|
213
|
+
Two players are no pick at all. A starter auto-subbed out had no
|
|
214
|
+
minutes, so his zero could never have scored in any XI; left in the
|
|
215
|
+
pool he would be picked over a team-mate on a red card and printed as
|
|
216
|
+
"benched" when he started. And a draft player the main game could not
|
|
217
|
+
be matched to carries a false zero, not a score (`unmatched`), so a
|
|
218
|
+
squad holding one has no best XI to reselect: every bench player would
|
|
219
|
+
"beat" him and the gain would be points the manager never left behind.
|
|
220
|
+
"""
|
|
221
|
+
if any(p["unmatched"] for p in squad):
|
|
222
|
+
return _NO_BETTER_XI
|
|
223
|
+
|
|
224
|
+
contributed = [p for p in squad if p["contributed"]]
|
|
225
|
+
actual_points = sum(p["points"] for p in contributed)
|
|
226
|
+
|
|
227
|
+
role = "is_captain" if captain_played else "is_vice_captain"
|
|
228
|
+
held = next((p for p in contributed if p[role]), None)
|
|
229
|
+
|
|
230
|
+
by_position: dict[str, list[RecapManagerPlayer]] = {"GK": [], "DEF": [], "MID": [], "FWD": []}
|
|
231
|
+
for p in squad:
|
|
232
|
+
if p["position"] in by_position and not p["auto_sub_out"]:
|
|
233
|
+
by_position[p["position"]].append(p)
|
|
234
|
+
# Highest scorer first, a starter ahead of a bench player on level points
|
|
235
|
+
# so the reselection never swaps for nothing.
|
|
236
|
+
for ranked in by_position.values():
|
|
237
|
+
ranked.sort(key=lambda p: (-p["points"], not p["contributed"]))
|
|
238
|
+
|
|
239
|
+
def _line(position: str, count: int) -> list[RecapManagerPlayer] | None:
|
|
240
|
+
ranked = by_position[position]
|
|
241
|
+
if len(ranked) < count:
|
|
242
|
+
return None
|
|
243
|
+
top = ranked[:count]
|
|
244
|
+
if held is None or held["position"] != position or any(p is held for p in top):
|
|
245
|
+
return top
|
|
246
|
+
return [held, *[p for p in ranked if p is not held][: count - 1]]
|
|
247
|
+
|
|
248
|
+
def _formation_xi(def_n: int, mid_n: int, fwd_n: int) -> list[RecapManagerPlayer] | None:
|
|
249
|
+
xi: list[RecapManagerPlayer] = []
|
|
250
|
+
for position, count in (("GK", 1), ("DEF", def_n), ("MID", mid_n), ("FWD", fwd_n)):
|
|
251
|
+
line = _line(position, count)
|
|
252
|
+
if line is None:
|
|
253
|
+
return None
|
|
254
|
+
xi.extend(line)
|
|
255
|
+
return xi
|
|
256
|
+
|
|
257
|
+
best_points = actual_points
|
|
258
|
+
best_xi: list[RecapManagerPlayer] | None = None
|
|
259
|
+
best_swaps = 0
|
|
260
|
+
for def_n, mid_n, fwd_n in VALID_FORMATIONS:
|
|
261
|
+
xi = _formation_xi(def_n, mid_n, fwd_n)
|
|
262
|
+
if xi is None:
|
|
263
|
+
continue
|
|
264
|
+
total = sum(p["points"] for p in xi)
|
|
265
|
+
swaps = sum(1 for p in xi if not p["contributed"])
|
|
266
|
+
if total > best_points or (total == best_points and best_xi is not None and swaps < best_swaps):
|
|
267
|
+
best_points, best_xi, best_swaps = total, xi, swaps
|
|
268
|
+
if best_xi is None:
|
|
269
|
+
return _NO_BETTER_XI
|
|
270
|
+
|
|
271
|
+
by_points = functools.partial(sorted, key=lambda p: -p["points"])
|
|
272
|
+
return BestXISelection(
|
|
273
|
+
gain=best_points - actual_points,
|
|
274
|
+
benched=tuple(by_points(p for p in best_xi if not p["contributed"])),
|
|
275
|
+
dropped=tuple(by_points(p for p in contributed if not any(p is q for q in best_xi))),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
162
279
|
# ---------------------------------------------------------------------------
|
|
163
280
|
# Classic data collection
|
|
164
281
|
# ---------------------------------------------------------------------------
|
|
@@ -455,6 +572,8 @@ async def _fetch_all_manager_data(
|
|
|
455
572
|
vice_captain_name = player.web_name
|
|
456
573
|
vice_captain_points = pts
|
|
457
574
|
|
|
575
|
+
best_xi = best_xi_selection(squad, captain_played=captain_played)
|
|
576
|
+
|
|
458
577
|
# Human-readable auto-sub descriptions
|
|
459
578
|
auto_sub_descriptions: list[str] = []
|
|
460
579
|
for sub in automatic_subs:
|
|
@@ -518,6 +637,7 @@ async def _fetch_all_manager_data(
|
|
|
518
637
|
active_chip=_CHIP_DISPLAY.get(active_chip, active_chip) if active_chip else None,
|
|
519
638
|
squad=squad,
|
|
520
639
|
bench_points=bench_points,
|
|
640
|
+
best_xi_gain=best_xi.gain,
|
|
521
641
|
transfer_cost=transfer_cost,
|
|
522
642
|
auto_subs=auto_sub_descriptions,
|
|
523
643
|
transfers=transfers,
|
|
@@ -1442,6 +1562,14 @@ def _captain_detail(
|
|
|
1442
1562
|
return detail + _omitted_suffix(omitted, "manager")
|
|
1443
1563
|
|
|
1444
1564
|
|
|
1565
|
+
def _name_points(players: Sequence[RecapManagerPlayer]) -> str:
|
|
1566
|
+
"""Render "Groß (17), Gvardiol (11) and Trafford (3)" for an award's prose."""
|
|
1567
|
+
named = [f"{p['name']} ({p['points']})" for p in players]
|
|
1568
|
+
if len(named) <= 1:
|
|
1569
|
+
return "".join(named)
|
|
1570
|
+
return ", ".join(named[:-1]) + " and " + named[-1]
|
|
1571
|
+
|
|
1572
|
+
|
|
1445
1573
|
def _compute_shared_awards(
|
|
1446
1574
|
managers: list[RecapManagerEntry],
|
|
1447
1575
|
format_name: str = "classic",
|
|
@@ -1481,36 +1609,39 @@ def _compute_shared_awards(
|
|
|
1481
1609
|
# Biggest bench haul — excludes Bench Boost managers (their bench counted).
|
|
1482
1610
|
# Detect via per-player flag to avoid coupling to the display-form chip string
|
|
1483
1611
|
# stored on RecapManagerEntry.active_chip (e.g. "BB" vs raw "bboost").
|
|
1612
|
+
# Ranked on what the bench actually cost -- the best legal XI's gain over
|
|
1613
|
+
# the one that played -- never on the bench's raw sum, which counts a
|
|
1614
|
+
# keeper who could not have played and outfielders no formation had room
|
|
1615
|
+
# for, and so overstated every week and picked the wrong manager (#350).
|
|
1484
1616
|
bench_candidates = [
|
|
1485
1617
|
m for m in managers
|
|
1486
1618
|
if not any(p.get("is_bench_boost_player") for p in m.get("squad", []))
|
|
1487
1619
|
]
|
|
1488
1620
|
if bench_candidates:
|
|
1489
|
-
|
|
1490
|
-
if
|
|
1491
|
-
bench_kings = [m for m in bench_candidates if m["
|
|
1621
|
+
best_gain = max(m["best_xi_gain"] for m in bench_candidates)
|
|
1622
|
+
if best_gain > 0:
|
|
1623
|
+
bench_kings = [m for m in bench_candidates if m["best_xi_gain"] == best_gain]
|
|
1492
1624
|
# Each entry is verbose, so a wide tie is capped the same way the
|
|
1493
|
-
# transfer and captain awards are. Everyone tied here
|
|
1494
|
-
# same points, so name the managers it actually cost:
|
|
1495
|
-
# the bench stings more on a 40-point week than an
|
|
1496
|
-
# Name is the secondary key so the choice stays
|
|
1625
|
+
# transfer and captain awards are. Everyone tied here left the
|
|
1626
|
+
# same points behind, so name the managers it actually cost:
|
|
1627
|
+
# leaving 12 on the bench stings more on a 40-point week than an
|
|
1628
|
+
# 80-point one. Name is the secondary key so the choice stays
|
|
1629
|
+
# deterministic.
|
|
1497
1630
|
bench_kings.sort(key=lambda m: (m["gw_points"], m["manager_name"]))
|
|
1498
1631
|
omitted = max(0, len(bench_kings) - _DETAIL_CAP)
|
|
1499
1632
|
detail_parts = []
|
|
1500
1633
|
for m in bench_kings[:_DETAIL_CAP]:
|
|
1501
|
-
|
|
1502
|
-
p for p in m["squad"]
|
|
1503
|
-
if not p["contributed"] and not p["auto_sub_out"] and p["points"] > 0
|
|
1504
|
-
]
|
|
1505
|
-
player_detail = ", ".join(f"{p['name']} ({p['points']})" for p in bench_players)
|
|
1634
|
+
swap = best_xi_selection(m["squad"], captain_played=m["captain_played"])
|
|
1506
1635
|
detail_parts.append(
|
|
1507
|
-
f"{m['manager_name']}
|
|
1508
|
-
f" (team scored {m['gw_points']} pts):
|
|
1636
|
+
f"{m['manager_name']} could have scored {m['best_xi_gain']} more with"
|
|
1637
|
+
f" their best XI (team scored {m['gw_points']} pts):"
|
|
1638
|
+
f" {_name_points(swap.benched)} benched while"
|
|
1639
|
+
f" {_name_points(swap.dropped)} played"
|
|
1509
1640
|
)
|
|
1510
1641
|
detail = "; ".join(detail_parts) + _omitted_suffix(omitted, "manager")
|
|
1511
1642
|
awards["biggest_bench_haul"] = RecapAwardEntry(
|
|
1512
1643
|
manager_name=" and ".join(m["manager_name"] for m in bench_kings),
|
|
1513
|
-
value=
|
|
1644
|
+
value=best_gain,
|
|
1514
1645
|
detail=detail,
|
|
1515
1646
|
)
|
|
1516
1647
|
|
|
@@ -2169,6 +2300,8 @@ async def collect_draft_recap_data(
|
|
|
2169
2300
|
),
|
|
2170
2301
|
))
|
|
2171
2302
|
|
|
2303
|
+
best_xi = best_xi_selection(squad, captain_played=False)
|
|
2304
|
+
|
|
2172
2305
|
# Build auto-sub descriptions
|
|
2173
2306
|
auto_sub_descs: list[str] = []
|
|
2174
2307
|
for s in subs:
|
|
@@ -2289,6 +2422,7 @@ async def collect_draft_recap_data(
|
|
|
2289
2422
|
active_chip=None,
|
|
2290
2423
|
squad=squad,
|
|
2291
2424
|
bench_points=bench_points,
|
|
2425
|
+
best_xi_gain=best_xi.gain,
|
|
2292
2426
|
transfer_cost=0,
|
|
2293
2427
|
auto_subs=auto_sub_descs,
|
|
2294
2428
|
transactions=manager_txns,
|
|
@@ -253,7 +253,19 @@ class RecapManagerEntry(TypedDict):
|
|
|
253
253
|
vice_captain_points: int
|
|
254
254
|
active_chip: str | None
|
|
255
255
|
squad: list[RecapManagerPlayer]
|
|
256
|
+
# The bench's raw points sum -- the API's own `points_on_bench`, which is
|
|
257
|
+
# what the ledger records at every tier. Not what the bench cost: only
|
|
258
|
+
# one keeper plays, and an outfielder can only come in for someone a
|
|
259
|
+
# legal formation lets him replace, so most of it was never available.
|
|
256
260
|
bench_points: int
|
|
261
|
+
# How many more points the best legal XI from the same 15 would have
|
|
262
|
+
# scored than the XI that counted, the doubled player held in place so
|
|
263
|
+
# the multiplier is not silently reassigned (`best_xi_selection()` in
|
|
264
|
+
# `_league_recap_data.py`). Zero when no reselection beats the one that
|
|
265
|
+
# played, however many points the bench scored -- the Biggest Bench
|
|
266
|
+
# award ranks on this, never on `bench_points` (issue #350). Never
|
|
267
|
+
# written to the ledger: it is derivable from `squad`.
|
|
268
|
+
best_xi_gain: int
|
|
257
269
|
transfer_cost: int
|
|
258
270
|
auto_subs: list[str]
|
|
259
271
|
transfers: NotRequired[list[RecapTransfer]]
|
|
@@ -801,3 +813,19 @@ def format_contested_claim(contest: RecapContestedClaim) -> str:
|
|
|
801
813
|
f"{contest['player']} was claimed by {contest['claimants']} managers: "
|
|
802
814
|
f"{contest['winner']} won him; {beaten} {verb} beaten to him."
|
|
803
815
|
)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def recap_title(gameweek: int, league_name: str | None) -> str:
|
|
819
|
+
"""The saved recap's H1: "Gameweek 4 Recap: Sunday League".
|
|
820
|
+
|
|
821
|
+
The one definition of the report's title, read by the template that
|
|
822
|
+
renders it and the prompt that tells the editorial not to write another
|
|
823
|
+
(issue #349). The league's name goes in exactly as configured -- it is
|
|
824
|
+
the line the reader identifies the recap by, so it is never the model's
|
|
825
|
+
to restyle. An empty name leaves just "Gameweek 4 Recap", and so does
|
|
826
|
+
None: the collectors default the name only when the API omits it, not
|
|
827
|
+
when it sends null, and a crash here is swallowed by the report agent
|
|
828
|
+
into a run that exits 0 having saved nothing.
|
|
829
|
+
"""
|
|
830
|
+
name = (league_name or "").strip()
|
|
831
|
+
return f"Gameweek {gameweek} Recap: {name}" if name else f"Gameweek {gameweek} Recap"
|
|
@@ -20,7 +20,7 @@ from fpl_cli.cli._json import (
|
|
|
20
20
|
output_format_option,
|
|
21
21
|
)
|
|
22
22
|
from fpl_cli.services.player_prior import early_season_quality_warning
|
|
23
|
-
from fpl_cli.services.scoring import normalise_score, pick_display_ceiling
|
|
23
|
+
from fpl_cli.services.scoring import VALID_FORMATIONS, normalise_score, pick_display_ceiling
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from fpl_cli.services.squad_allocator import ScoredPlayer, SquadResult
|
|
@@ -160,7 +160,7 @@ def allocate_command(
|
|
|
160
160
|
raise SystemExit(1)
|
|
161
161
|
bb_gw_idx = bench_boost_gw - start_gw
|
|
162
162
|
|
|
163
|
-
with console.status("Solving (
|
|
163
|
+
with console.status(f"Solving ({len(VALID_FORMATIONS)} formations)..."):
|
|
164
164
|
result = solve_squad(
|
|
165
165
|
scored_players, coefficients, budget,
|
|
166
166
|
bench_discount=bd, bench_boost_gw_idx=bb_gw_idx,
|
|
@@ -879,6 +879,7 @@ async def _recap_llm_summarise(
|
|
|
879
879
|
format_recap_transfers_context,
|
|
880
880
|
format_recap_waivers_context,
|
|
881
881
|
get_recap_synthesis_prompt,
|
|
882
|
+
normalise_recap_editorial,
|
|
882
883
|
)
|
|
883
884
|
|
|
884
885
|
# Setup debug directory
|
|
@@ -935,7 +936,14 @@ async def _recap_llm_summarise(
|
|
|
935
936
|
prompt=user_prompt,
|
|
936
937
|
system_prompt=system_prompt,
|
|
937
938
|
)
|
|
938
|
-
|
|
939
|
+
# The report writes its own title; whatever heading the model
|
|
940
|
+
# opened with anyway is demoted beneath it or, when it only
|
|
941
|
+
# restates the title, dropped (#349). The JSON payload carries
|
|
942
|
+
# the same text, so a consumer sees the editorial as saved.
|
|
943
|
+
collected_data["synthesis_summary"] = normalise_recap_editorial(
|
|
944
|
+
synthesis_provider.post_process(synthesis_result.content),
|
|
945
|
+
league_name=collected_data["league_name"],
|
|
946
|
+
)
|
|
939
947
|
if synthesis_result.stopped_early:
|
|
940
948
|
# The recap's editorial goes into a saved report too, so a
|
|
941
949
|
# truncated one must not look finished. Recorded for the JSON
|