fplkit 2.6.1__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/TOOLS.md +2 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/AGENTS.md +1 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/CHANGELOG.md +8 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/CLAUDE.md +1 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/PKG-INFO +15 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/README.md +14 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/docs/architecture.md +11 -9
- {fplkit-2.6.1 → fplkit-2.7.0}/docs/command-reference.md +97 -19
- {fplkit-2.6.1 → fplkit-2.7.0}/docs/fpl-rules.md +11 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/_version.py +2 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/orchestration/report.py +35 -16
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/fpl_draft.py +44 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_fines.py +61 -8
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_league_recap_data.py +231 -53
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_league_recap_history.py +534 -61
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_league_recap_types.py +257 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_review_analysis.py +16 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_review_classic.py +15 -4
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_review_draft.py +67 -7
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_review_summarisation.py +119 -20
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/league_recap.py +5 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/review.py +23 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/status.py +25 -14
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/league_history.py +57 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/prompts/league_recap.py +85 -15
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/prompts/review.py +419 -21
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/league_history.py +29 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/templates/gw_league_recap.md.j2 +3 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/templates/gw_review.md.j2 +17 -7
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_draft.py +113 -13
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_doctor.py +19 -19
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_format_suppression.py +2 -4
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_league_recap.py +1234 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_player.py +2 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_review.py +367 -13
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_status.py +45 -8
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_common.py +2 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_fines.py +87 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_league_history_counters.py +24 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_league_history_store.py +141 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_league_recap.py +764 -23
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_report_agent.py +159 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_review.py +440 -4
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_scraper.py +2 -2
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/README.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/references/output-template.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/squad-builder/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.claude/settings.json +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.env.example +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.github/workflows/ci.yml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.github/workflows/release.yml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/.gitignore +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/CONTRIBUTING.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/LICENSE +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/cliff.toml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/docs/custom-analysis.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/_models.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_context.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_helpers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/preview.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/constants.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/player.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/team.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/models/types.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/parsers/recommendations.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/paths.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/season.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/league_history_notes.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/constants.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/single_gw.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/markdown.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/text.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/pyproject.toml +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/pyrightconfig.json +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/__init__.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/conftest.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_action.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_base.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_data.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_allocate.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_client.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_football_data.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_historical.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_understat.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_banner.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_captain.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_chips.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_format.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_history.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_init.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_intel.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_json.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_league.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_preview.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_squad.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_stats.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_targets.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_cli_xg.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_common_scoring.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_dependencies.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_fines_config.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_init.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_init_fines.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_league_history_notes.py +1 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_matchup_service.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_models.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_paths.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_player_clubs.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_player_prior.py +1 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_player_resolution.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_preseason.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_providers.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_recommendations_parser.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_report_paths.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_scoring.py +1 -1
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_season.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_season_awareness.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_season_previews.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_sell_prices.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_stats.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_team_ratings.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_utils_markdown.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_utils_text.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_utils_time.py +0 -0
- {fplkit-2.6.1 → fplkit-2.7.0}/tests/test_validate_draft_waivers.py +0 -0
|
@@ -47,8 +47,8 @@ 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 superseded line, 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. 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. 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
|
-
| `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. 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 |
|
|
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 |
|
|
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 |
|
|
54
54
|
| `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (CV-xGI percentile, 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 matchup and consistency bonuses stay pure observation, and the score agrees with `fpl transfer-eval`'s Outlook column; 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 | Yes | Classic | Yes | via-agent |
|
|
@@ -90,7 +90,7 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
|
|
|
90
90
|
- Draft has no captains, no budget, no transfers and no chips — acquisition is via waivers and free agents
|
|
91
91
|
|
|
92
92
|
## Rules
|
|
93
|
-
- This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits, PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir
|
|
93
|
+
- This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits (test fixtures included), PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir`, including in an issue's repro example — copy its shape, not its names. Footballer and club names are fine — they are public data. Before pushing, check the branch for a pre-existing leak and flag it rather than pushing over it.
|
|
94
94
|
- Verify before asserting: don't state that a command, file, function, or data point exists without checking first (read the file, run the command, grep for the name)
|
|
95
95
|
- Reviewing a PR by number: the session's working branch is never the PR's branch (a review session gets its own throwaway branch off main), so local files can silently be at a different commit than the PR — possibly missing the PR's changes entirely, or, worse, containing unrelated already-merged work that looks plausible but isn't in the diff. Ground every finding in the PR's actual diff (fetch via the GitHub API/`gh`, or check out the PR's head commit) before citing a file or line number, and re-verify that grounding before posting comments — don't trust a subagent's file/line citations without checking they match a file the PR's `get_files` list actually contains.
|
|
96
96
|
- Find-and-replace: review each replacement in context - don't blindly replace substrings in unrelated identifiers
|
|
@@ -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.6.1] - 2026-09-07
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- raise synthesis max_tokens default to 8000 (#321)
|
|
9
|
+
- say in the saved review when the analysis call never landed (#322)
|
|
10
|
+
- keep team ratings through a season that overruns the July cutover (#323)
|
|
11
|
+
|
|
4
12
|
## [2.6.0] - 2026-09-07
|
|
5
13
|
|
|
6
14
|
### Bug Fixes
|
|
@@ -90,7 +90,7 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
|
|
|
90
90
|
- Draft has no captains, no budget, no transfers and no chips — acquisition is via waivers and free agents
|
|
91
91
|
|
|
92
92
|
## Rules
|
|
93
|
-
- This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits, PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir
|
|
93
|
+
- This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits (test fixtures included), PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir`, including in an issue's repro example — copy its shape, not its names. Footballer and club names are fine — they are public data. Before pushing, check the branch for a pre-existing leak and flag it rather than pushing over it.
|
|
94
94
|
- Verify before asserting: don't state that a command, file, function, or data point exists without checking first (read the file, run the command, grep for the name)
|
|
95
95
|
- Reviewing a PR by number: the session's working branch is never the PR's branch (a review session gets its own throwaway branch off main), so local files can silently be at a different commit than the PR — possibly missing the PR's changes entirely, or, worse, containing unrelated already-merged work that looks plausible but isn't in the diff. Ground every finding in the PR's actual diff (fetch via the GitHub API/`gh`, or check out the PR's head commit) before citing a file or line number, and re-verify that grounding before posting comments — don't trust a subagent's file/line citations without checking they match a file the PR's `get_files` list actually contains.
|
|
96
96
|
- Find-and-replace: review each replacement in context - don't blindly replace substrings in unrelated identifiers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fplkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: CLI tool for Fantasy Premier League analysis - classic and draft formats
|
|
5
5
|
Project-URL: Homepage, https://github.com/rossgroomio/fpl-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/rossgroomio/fpl-cli
|
|
@@ -119,7 +119,20 @@ in its own warning callout. `league-recap --summarise` does the same for its edi
|
|
|
119
119
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
120
120
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
121
121
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
122
|
-
beneath the table, because a zero there means "not known", not "not fined".
|
|
122
|
+
beneath the table, because a zero there means "not known", not "not fined". A tie for
|
|
123
|
+
last is shared: everyone level on the lowest score of the gameweek pays, rather than
|
|
124
|
+
whichever of them the standings listed first. The ledger is append-only, so gameweeks
|
|
125
|
+
recorded before that was true are repaired by the next recap rather than re-ruled.
|
|
126
|
+
|
|
127
|
+
For a draft league the recap also records the waiver claims each manager lost to a rival,
|
|
128
|
+
kept apart from the moves that landed, so being outbid is never written into the ledger
|
|
129
|
+
as sitting the week out. Gameweeks recorded before that existed are named on stderr;
|
|
130
|
+
`fpl league-recap --draft --backfill-detail` re-records them while the draft API still
|
|
131
|
+
serves the season's transactions. Those lost claims also make the waiver wire's races
|
|
132
|
+
visible to the editorial — every player more than one manager claimed, who won him, who
|
|
133
|
+
was beaten to him and at what priority — and a week where three or more managers claimed
|
|
134
|
+
the same player gets a Most Contested award beside Waiver Genius and Waiver Disaster, so
|
|
135
|
+
a pickup four managers fought over no longer reads as an unremarkable move.
|
|
123
136
|
|
|
124
137
|
The recap prints the season table only at GW19 and at the finale, so the set-piece
|
|
125
138
|
stays a set-piece — though with `--summarise` its editorial knows the running totals
|
|
@@ -84,7 +84,20 @@ in its own warning callout. `league-recap --summarise` does the same for its edi
|
|
|
84
84
|
Fines are ruled per gameweek by `league-recap` and recorded against it, so `fpl
|
|
85
85
|
league-fines` reads them straight back off disk — no network, and any season still
|
|
86
86
|
on disk (`--season 2025-26`). Every gameweek that could not be ruled is named
|
|
87
|
-
beneath the table, because a zero there means "not known", not "not fined".
|
|
87
|
+
beneath the table, because a zero there means "not known", not "not fined". A tie for
|
|
88
|
+
last is shared: everyone level on the lowest score of the gameweek pays, rather than
|
|
89
|
+
whichever of them the standings listed first. The ledger is append-only, so gameweeks
|
|
90
|
+
recorded before that was true are repaired by the next recap rather than re-ruled.
|
|
91
|
+
|
|
92
|
+
For a draft league the recap also records the waiver claims each manager lost to a rival,
|
|
93
|
+
kept apart from the moves that landed, so being outbid is never written into the ledger
|
|
94
|
+
as sitting the week out. Gameweeks recorded before that existed are named on stderr;
|
|
95
|
+
`fpl league-recap --draft --backfill-detail` re-records them while the draft API still
|
|
96
|
+
serves the season's transactions. Those lost claims also make the waiver wire's races
|
|
97
|
+
visible to the editorial — every player more than one manager claimed, who won him, who
|
|
98
|
+
was beaten to him and at what priority — and a week where three or more managers claimed
|
|
99
|
+
the same player gets a Most Contested award beside Waiver Genius and Waiver Disaster, so
|
|
100
|
+
a pickup four managers fought over no longer reads as an unremarkable move.
|
|
88
101
|
|
|
89
102
|
The recap prints the season table only at GW19 and at the finale, so the set-piece
|
|
90
103
|
stays a set-piece — though with `--summarise` its editorial knows the running totals
|
|
@@ -388,9 +388,9 @@ Services live in `fpl_cli/services/` and provide the computation layer between a
|
|
|
388
388
|
| `squad_allocator` | ILP squad allocator (PuLP CBC), horizon-aware, chip-aware |
|
|
389
389
|
| `returnee_radar` | Injury returnee radar behind `fpl returnees`. Parses the FPL `news` grammar into a `ReturnSignal` (four shapes, only two of them dated), filters on a window and a **source-aware** quality bar, and diffs the result against a persisted snapshot for week-over-week transitions. The bar is source-aware because `player_prior` only assigns `source="history"` at 450+ minutes in the previous season and caps `prior_strength` at 0.5 on the `"price"` fallback, so one flat threshold would exclude exactly the players who missed most of last season through injury: history-sourced players are gated on `prior_strength`, price-sourced ones are scored through `value_quality` over their most recent season carrying real minutes, and the within-position price percentile is the last resort. That season supplies the DEF/GK weight terms (defensive contribution, saves, clean sheets, xGC per 90) from the `SeasonHistory` row when its source published them, and otherwise shrinks the ceiling via `ceiling_attainability()` rather than reading the gap as a measured zero — without which no keeper and effectively no defender could clear the bar (#132). `run_radar()` is the public entry point (snapshot I/O included); `build_radar()` is the pure core. The `SnapshotStore` behind it holds two states — the baseline every run in the current gameweek diffs against, and the current gameweek's own, promoted to baseline when the gameweek moves on — so a second run in one gameweek reports the same transitions as the first instead of diffing against what that run just stored (#225). **Fetches nothing** — the caller injects historical profiles and Understat season data, because `prepare_scoring_data` discards the profiles it built priors from and skips fetching them at all on a cache hit. Optional AI-search enrichment (`--enrich`) attaches an `EnrichedReturn` beside the FPL signal, never over it, and only a *cited* enriched date can decide an escalation |
|
|
390
390
|
| `team_form` | Rolling team form stats (last 6 matches, venue splits) |
|
|
391
|
-
| `league_history` | Durable league-history ledger: one NDJSON file per gameweek under `<data dir>/league_history/<season>/<format>-<league id>/`, written as a side effect of `league-recap`. Append-only — an identical row writes nothing, a differing one appends a superseding line, and readers resolve by highest fidelity tier then latest capture, with an unknown row ranking below every tier. Each row carries its own schema version: an older one is upgraded in memory on read (the line on disk is never rewritten, so a store shared with an older install stays readable by it), a newer one is skipped with a warning and preserved byte-for-byte. Two deliberate inversions of house convention: loading **fails closed** (unlike `chip_plan`, which resets on a corrupt file) because the API cannot rebuild a past gameweek — scoped to that one gameweek, whose coverage entry carries the file path and the `mv` that retires it, so a caller reports it as `league_history_store_unreadable` rather than as a coverage gap `--backfill-detail` would refuse to fill, and reports it once per run however many of a recap's readers hit the same file — and season is a **partition key** (unlike `team_ratings`, which discards a previous season) because per-gameweek granularity is destroyed at the July rollover. The consequence of partitioning is that the store only ever grows — nothing prunes a prior season, by design — at roughly a few megabytes per league per season |
|
|
391
|
+
| `league_history` | Durable league-history ledger: one NDJSON file per gameweek under `<data dir>/league_history/<season>/<format>-<league id>/`, written as a side effect of `league-recap`. Append-only — an identical row writes nothing, a differing one appends a superseding line, and readers resolve by highest fidelity tier then latest capture, with an unknown row ranking below every tier. Each row carries its own schema version: an older one is upgraded in memory on read (the line on disk is never rewritten, so a store shared with an older install stays readable by it), a newer one is skipped with a warning and preserved byte-for-byte — and every line the store writes is stamped with the writing install's version whatever version the row was read at, since a superseding line copied off a version-5 row but carrying a version-6 field would be validated by a version-5 install rather than skipped, and rejected as a corrupt file. Two deliberate inversions of house convention: loading **fails closed** (unlike `chip_plan`, which resets on a corrupt file) because the API cannot rebuild a past gameweek — scoped to that one gameweek, whose coverage entry carries the file path and the `mv` that retires it, so a caller reports it as `league_history_store_unreadable` rather than as a coverage gap `--backfill-detail` would refuse to fill, and reports it once per run however many of a recap's readers hit the same file — and season is a **partition key** (unlike `team_ratings`, which discards a previous season) because per-gameweek granularity is destroyed at the July rollover. The consequence of partitioning is that the store only ever grows — nothing prunes a prior season, by design — at roughly a few megabytes per league per season |
|
|
392
392
|
| `league_history_counters` | Declarative streak-condition registry (9 conditions: `weeks_on_top`, `bottom_half_run`, `gw_win_streak`, `gw_loss_streak`, `green_arrow_drought` shared; `captain_blank_run`, `hit_run` classic-only; `waiver_win_run`, `waiver_burn_run` draft-only) and the rebuildable counters projection it drives. Each entry declares the minimum run worth surfacing as a streak, `None` for the two conditions (bottom half, green-arrow drought) whose run only drives their season count's firing rule. Each predicate returns extend/reset/hold for one manager's row — hold on an unknown row, a fixture-less blank, a gameweek the condition does not apply to, or one it structurally could not have ruled (a green-arrow drought holds for a gameweek that began in first place, where climbing was impossible), so neither a capture gap nor an impossibility ever lies about a streak in either direction. Alongside each currently-open run, the state carries season-wide occurrence totals (issue #164): every extending gameweek counts once and survives a reset, holds are tallied separately as the count's "not judged" qualifier, and the last-occurrence and first-evaluated gameweeks bound what a consumer may claim — so "their sixth gameweek win of the season" is derivable, not just the open run. Each entry also declares a `CountSurfacePolicy`, the condition's own rule for when that total is worth rendering weekly (U9 applies it; the milestone set-pieces and `--format json` ignore it). The projection is a disposable cache (never a second source of truth) under `<data dir>/league_history_counters/<season>/<format>-<league id>/counters.json`: it advances one gameweek at a time when its stamp is exactly one behind, and **fails open** to a full rebuild from `league_history`'s rows otherwise — missing, unreadable, wrong-version, or a stamp that isn't stamp+1 (which is also what makes the season totals retroactive: the version bump that added them rebuilds silently from rows already on disk) |
|
|
393
|
-
| `league_history_fines` | Season fine tally (`build_season_fines_tally()`): a pure fold over `league_history`'s rows into per-manager, per-rule counts, with no cache and no second source of truth. Counts only what a row already recorded, so a settings change moves future rulings and leaves history alone — a guarantee `_freeze_recorded_fines` (`cli/_league_recap_history.py`) carries into backfill, where a repair would otherwise re-rule an unchanged cohort under whatever config is current at repair time. Every gameweek that could not be ruled is named as a qualifier rather than folded in as a zero — never captured, unreadable, captured but reaching nobody (every row unknown), holding no record of what was ruled (schema version < 4, or an evaluation that raised), recording a ruling on no rules at all, or captured at a fidelity that could not rule a given rule. Each qualifier states only what the ledger can prove: where two causes are indistinguishable from a row, neither is named. A mid-season joiner keeps their real, lower totals and is qualified; a manager who has since left keeps the fines already ruled against them, bounded to the gameweeks they were recorded for. Rulings are tracked per rule as well as per manager (`unruled_gameweeks_for()`), since a coarse gameweek rules `last-place` and structurally cannot rule `red-card` — the recap prompt asks that question before it will call a fine a manager's first of the season |
|
|
393
|
+
| `league_history_fines` | Season fine tally (`build_season_fines_tally()`): a pure fold over `league_history`'s rows into per-manager, per-rule counts, with no cache and no second source of truth. Counts only what a row already recorded, so a settings change moves future rulings and leaves history alone — a guarantee `_freeze_recorded_fines` (`cli/_league_recap_history.py`) carries into backfill, where a repair would otherwise re-rule an unchanged cohort under whatever config is current at repair time. Every gameweek that could not be ruled is named as a qualifier rather than folded in as a zero — never captured, unreadable, captured but reaching nobody (every row unknown), holding no record of what was ruled (schema version < 4, or an evaluation that raised), recording a ruling on no rules at all, or captured at a fidelity that could not rule a given rule. Each qualifier states only what the ledger can prove: where two causes are indistinguishable from a row, neither is named. A mid-season joiner keeps their real, lower totals and is qualified; a manager who has since left keeps the fines already ruled against them, bounded to the gameweeks they were recorded for. Rulings are tracked per rule as well as per manager (`unruled_gameweeks_for()`), since a coarse gameweek rules `last-place` and structurally cannot rule `red-card` — the recap prompt asks that question before it will call a fine a manager's first of the season. `last-place` is the one cohort-relative rule, so it is the one a tie falls on: every manager level on the gameweek's lowest score is fined, and `_repair_tied_last_place` sweeps gameweeks recorded before that was true, appending the ruling the gameweek already wrote against the managers it left out rather than re-ruling anything (issue #336) |
|
|
394
394
|
| `league_history_notes` | Notes pack (`build_notes_pack()`), season-phase marker (`derive_season_phase()`, standalone) and the `is_season_milestone()` predicate the recap gates its *printed* season fines table on (its prompt section is ungated) - the phase's *first* gameweek and the finale, deliberately not the whole 13-gameweek midpoint phase, since a once-a-season set-piece fired thirteen times is wallpaper built from `league_history_counters`'s projection plus a bounded trailing window of raw ledger rows (`TRAILING_WINDOW_GAMEWEEKS = 6`, reused as the run-in phase's own length). Every open streak renders as an observed count over its true span rather than "in a row" once any gameweek held (e.g. "3 in the last 11, with 8 not recorded"); each entry declares which of console/report/prompt it reaches — a reportable streak reaches all three, coverage statements reach report+prompt, the season-phase marker is prompt-only (scene-setting for the editorial, never printed in the report — issue #187), and a below-minimum streak is retained (exposed via `--format json`) with no surfaces. Season-count entries (issue #164) render each condition's season occurrence total over the manager's own evaluated span, with holds stated as "not judged either way", surfaced per the registry's own declarative `CountSurfacePolicy` (a step the total must land on, run-length milestones read off the open run rather than the total, a first-occurrence rule gated on the season's half, a whole-condition second-half gate, and a ride-along rule for same-gameweek peers — an absolute floor for counts that stay small, a window relative to the nearest firing total for counts that climb all season). Evaluation is two-pass because firing is cross-manager: who fired their condition this gameweek, then which same-gameweek incrementers ride along beside them. Every nonzero count is retained for `--format json`, and at the two `is_season_milestone()` gameweeks the whole nonzero set carries report+prompt — the report's `## Season Counts` set-piece and the retrospective editorial's season-spanning facts. A "since GW X" qualifier is stated only when a partition's or a specific manager's own recorded coverage genuinely begins later than the league's start gameweek, never merely because a trailing window was read. Only the finale phase rescans every captured gameweek (via `rebuild_counters_through`, never the cached `compute_counters_through`) so weekly cost stays flat as the season progresses |
|
|
395
395
|
|
|
396
396
|
## LLM Provider Abstraction
|
|
@@ -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), 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
|
|
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)
|
|
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)
|
|
@@ -547,7 +547,7 @@ fpl_cli/
|
|
|
547
547
|
│ ├── contract.py # Runtime tripwires: CSV header checks + empty-output warnings for dataset drift
|
|
548
548
|
│ ├── dataset_fetcher.py # DatasetFetcher (disk cache with ETag/TTL for GitHub CSVs)
|
|
549
549
|
│ ├── fpl.py # FPLClient (main API, caches bootstrap-static)
|
|
550
|
-
│ ├── fpl_draft.py # FPLDraftClient + match_draft_to_main()
|
|
550
|
+
│ ├── fpl_draft.py # FPLDraftClient + match_draft_to_main() + the transaction `result` vocabulary (`is_accepted_transaction()` / `is_lost_claim()`, issue #329). The feed carries denied claims alongside accepted ones and the two denial codes differ: `di` is a claim a rival won -- a genuine attempt -- while `do` means the manager's own earlier accepted claim had already consumed the drop, a conditional-chain cascade that is never an attempt. Consumers filter through these two predicates rather than comparing `result` themselves, so a move ledger never counts a denied row and an activity question never misses one
|
|
551
551
|
│ ├── understat.py # UnderstatClient + match_fpl_to_understat(), understat_club_rows() (the club gate the matcher and `fpl doctor --providers` share), understat_join_warnings(), understat_name_join_stats() (the name-level join rate, the signal a silently unmatched player had none of), decode_entities() (Understat escapes its payload for HTML, so it is decoded once at the fetch boundary), matches_in_season() (the season guard behind get_team())
|
|
552
552
|
│ ├── historical_types.py # Shared dataclasses (SeasonHistory — incl. the four optional per-90 DEF/GK rates only Core-Insights publishes, PlayerProfile, GwTrendProfile) + compute_trend/compute_acceleration/compute_reliability
|
|
553
553
|
│ ├── vaastav.py # VaastavClient (the two oldest seasons of the window via DatasetFetcher)
|
|
@@ -590,13 +590,13 @@ fpl_cli/
|
|
|
590
590
|
│ ├── team.py # Team
|
|
591
591
|
│ ├── fixture.py # Fixture
|
|
592
592
|
│ ├── chip_plan.py # ChipPlan, ChipType, PlannedChip, UsedChip
|
|
593
|
-
│ ├── league_history.py # LeagueHistoryRow + Ledger* sub-models, CaptureStatus, FidelityTier, schema version constants (v4 adds `fine_rules_evaluated`, which tells an unfined gameweek from an unruled one; v5 adds `LedgerFine.players`, so a ruling holds references to the players it names rather than only their names as prose); ConditionRunState + LeagueHistoryCountersProjection for the counters cache; ManagerEarliestGameweekCache for the first-captured-gameweek memo
|
|
593
|
+
│ ├── league_history.py # LeagueHistoryRow + Ledger* sub-models, CaptureStatus, FidelityTier, schema version constants (v4 adds `fine_rules_evaluated`, which tells an unfined gameweek from an unruled one; v5 adds `LedgerFine.players`, so a ruling holds references to the players it names rather than only their names as prose; v6 adds `lost_claims`, the waiver claims a draft manager lost to a rival, kept apart from `transactions` because nothing moved and `None` wherever nothing was recorded); ConditionRunState + LeagueHistoryCountersProjection for the counters cache; ManagerEarliestGameweekCache for the first-captured-gameweek memo
|
|
594
594
|
│ └── types.py # TypedDicts: CaptainCandidate, WaiverTarget, EnrichedPlayer, etc.
|
|
595
595
|
├── prompts/
|
|
596
596
|
│ ├── scout.py # ScoutAgent system/user prompts
|
|
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`. 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
|
|
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`. 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. `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
|
|
600
600
|
├── parsers/
|
|
601
601
|
│ └── recommendations.py # Parse gw{N}-recommendations.md into structured decisions
|
|
602
602
|
├── scraper/
|
|
@@ -704,11 +704,13 @@ A settings block present but empty — every line under it commented out — par
|
|
|
704
704
|
- **So does the reason a command gave up.** Table mode used to have no rule about it, and the messages landed wherever each call site happened to put them: `emit_failure` defaulted to stdout and `handle_agent_failure` used it too, a handful of call sites passed `stream=error_console` to opt out, and the hand-rolled prints next to a `raise SystemExit(1)` went both ways. `fpl squad grid` reported a missing entry ID on stdout while `fpl squad sell-prices` reported a missing cache on stderr — two subcommands of one group, both naming a missing prerequisite — and `fpl player` used one stream for a name that matched nothing and the other for a fetch that failed (issue #162). `2>/dev/null` is how a user quietens a CLI, so the split decided, per command and unpredictably, whether that discarded the explanation or the output. `emit_failure` now writes to `error_console` for everyone and takes no stream argument, `handle_agent_failure` matches it, and the hand-rolled sites that precede a nonzero exit follow. The rule is the one `emit_json_error`'s docstring already claimed for JSON: stdout is what was asked for, stderr is why there is none of it. `tests/test_cli_failure_streams.py` walks the command tree against an unreachable API and holds every command to it, so a new one inherits the rule rather than choosing again. Prose that accompanies exit 0 is untouched — a command that reports a problem and still succeeds has output to interleave with, and the few that report one and exit 0 anyway were a separate defect, since narrowed. Where a command has a `--format json` path that calls the same condition an error, the table path exits 1 too and says the same thing: `review` and `league-recap` on a gameweek neither could resolve (issue #273), then `history` on a provider that refused, `chips timing` with no `classic_entry_id` and `waivers` with no `draft_league_id` (issue #286) — all through `emit_failure`. That subset stayed open as long as it did because both existing walks treat "did not exit 1" as nothing to assert, so the softened table half passed the stream walk while the strict JSON half passed the envelope walk beside it; `tests/test_cli_failure_parity.py` is a third walk over the same commands that compares the two answers rather than asserting either. The reason itself travels the same way: an envelope carrying a fixed `Failed to fetch historical data` while the terminal got the upstream's own message is the same defect one layer in, since the consumer told to script against `error` is the reader who cannot see the other stream — `history`, `price-history` and `chips timing`'s fixture agent all pass the cause through now, and the same walk holds every command to it. Where the two formats still disagreed after that, they were describing different events rather than wording one differently: `differentials` fetched the next gameweek before running its agent under `--format json` and not at all in table mode, so an outage died in the client on one path and in the agent on the other. The fetch stamps metadata onto a payload that does not exist yet, so it now happens after the agent has succeeded. What is left of the defect is the commands with no JSON path at all — `chips sync`, `chips add`, `preview` — which still print their reason to stdout and exit 0.
|
|
705
705
|
- **A command's own config is a failure it has to catch, like an outage.** `api_failure_boundary` had covered the unreachable-upstream half since #159, and the other half went uncovered for the same reason it was easy to miss: a config parser runs deep inside a command body, far from the code that knows the command name and the output format, and it raises a plain `ValueError` no caller was catching. One `fines:` rule missing its `threshold:` therefore escaped click as a traceback from `fpl league-fines`, `fpl status` and `fpl league-recap` alike — the settings are read on nearly every command — and under `--format json` that is exit 1 with zero bytes on stdout, which a consumer cannot tell from a hang or a killed process (issue #170). The messages themselves were good; they just never reached the user through a supported channel. Parsers now raise `ConfigError` (`cli/_context.py`) and commands carry `config_failure_boundary` (`cli/_json.py`), which reports it through `emit_failure` — envelope or red prose, whichever the reader is on. It is a decorator rather than a context manager because there is no single seam to wrap the way `asyncio.run()` is one for an outage: `status` parses fines in two branches and `league-recap` reaches its parse through three helpers, so the callback is the only wrapper that covers the body wherever the parse happens; it reads the command name and format off the click context so a command cannot name itself one thing there and another in its `emit_json`. `ConfigError` subclasses `ValueError` so callers already catching one are unaffected, and the boundary catches nothing wider — a bug in our own code still surfaces as a traceback. Failing rather than degrading is the decision: an unreadable `fines:` block is not "no fines configured", and `league-recap` treating it as such would stamp an empty `fine_rules_evaluated` into the append-only ledger, the false acquittal #136 exists to prevent. Which is also why the parser validates more than the rule types it started with: an unrecognised key under `fines:` (`clasic:`) and a `threshold:` that is not a number both used to reach that same ledger row rather than the user -- the first by emptying the rule list into what reads downstream as "nothing configured", the second as a `TypeError` that `status` catches and drops the section on, and `evaluate_league_fines` logs per manager. A parser that only raises where a value is *used* is what makes the boundary worth having.
|
|
706
706
|
- **A tie in name resolution is an error, not a coin toss.** `resolve_player` (`models/player.py`) picks one player, so its two match tiers cannot mean the same thing. Several *substring* matches is a fuzzy shortlist and first-wins stays the contract ("Bru" -> De Bruyne). Several *exact* matches is a genuine tie — Dean and Jordan Henderson are both `Henderson` — and there taking the head silently resolved to the lower element id, so `bench_order.py` scored a player the squad did not own (issue #180). It now raises `AmbiguousPlayerError`, naming every candidate and the `Name (TEAM)` form that picks one. `resolve_players` (plural) is unaffected: returning the tie *is* its answer. Same reasoning as `season_previews.resolve_name`, which reports ambiguity rather than guessing. `resolve_player_or_report` / `resolve_players_or_report` wrap that for the commands and gw-prep scripts taking names as user input, turning both ways of missing a single player -- nothing matched, several did -- into one labelled message the caller shows. They live in the package, not beside the scripts, because the dependency runs one way: the skill's scripts import `fpl_cli`, never the reverse.
|
|
707
|
+
- **A tie the cohort cannot break is shared, never settled by arrival order.** Two managers level on a gameweek score are indistinguishable from anything the gameweek records — classic breaks a points tie on fewest transfers season-to-date (`docs/fpl-rules.md`), which is not a gameweek fact — so every cohort-relative reading shares the place rather than picking one. `derive_point_in_time_positions` numbers a tie 1, 2, 2, 4 (issues #163, #230); `_gw_loss_streak` (`services/league_history_counters.py`) counts a last-place finish for everyone on the minimum; and the `last-place` fine now does the same. It did not: every caller built `worst_performers` from a single-winner `min()`, so which of a tied pair paid was decided by the cohort's standings order, which moves through the season — and the other was recorded with `fines: []`, a positive "nothing was owed" that `fpl league-fines` read back as an acquittal (issue #336). `_joint_last` (`cli/_fines.py`) derives the tied group from the list it is handed, so a caller passing its bottom-five display list (`review`) and one passing the tied group outright (`league-recap`) get the same answer. The fix could not stop at the rule: the ledger is append-only and rulings are frozen at capture, so gameweeks already recorded wrong are unreachable by re-running. `_repair_tied_last_place` sweeps them on every recap, appending the ruling that gameweek already wrote — penalty text and all, re-keyed — against the managers it left out, which is a repair rather than a re-ruling and so cannot be moved by a settings change since. Which measure ruled the gameweek is not on the row, so it is inferred rather than read from today's `use_net_points`: a measure under which some ruled manager scored lower than the manager the gameweek fined cannot be the one it used. Where both survive that test and disagree about who was level, the manager is named (`league_history_tied_last_place_ambiguous`) rather than fined on the measure that may not have ruled — a repair that invented a fine would be worse than the miss, and going quiet would leave a gameweek that settled half its tie indistinguishable from one that settled all of it. Counted per manager throughout, never per recorded fine: nothing rejects a `fines:` block configuring `last-place` twice, so one manager can hold several for one gameweek.
|
|
707
708
|
- **A status code is not a diagnosis, and it is diagnosed once.** A 404 on `entry/<id>/event/<gw>/picks/` has two causes — the squad is not picked yet, or the entry ID is wrong, the likelier one given classic entry IDs are reissued every season — and each command that reads your own squad met both. `fpl squad` asserted the first for either, so a stale ID sent the reader to look at the calendar; `fpl captain` reported the raw status and path, so the identical config read as an API problem one command later (#228). `get_own_squad_picks` (`agents/common.py`) wraps `get_actual_squad_picks`, checks `entry/<id>/` on a 404 and raises `SquadPicksUnavailableError` carrying the answer, so `fpl squad`, `fpl squad grid` and the agent show one sentence rather than two wordings of one problem — the argument `require_entry_id` already makes for the missing-ID message. Only a 404 on the entry condemns the ID; the probe's own failure, of any kind, falls back to the pre-deadline wording rather than costing the caller the report it already had.
|
|
708
709
|
- **Main-keyed lookups are translated once, not read across the id boundary.** The draft and main bootstraps number their elements independently: they agree for most of the pool and drift apart at the tail, where each has appended new registrations in its own order (59 of 652 players disagreed the day #209 was fixed). Every id-keyed lookup `prepare_scoring_data` builds — histories, priors, the fixture-adjusted npxG lookup, the consistency lookup — is keyed by the main element id, so a draft agent reading one with a draft id does not miss, it lands on a *different* real player and scores the target on a stranger's season. `rekey_for_draft` (`agents/common.py`) re-keys the whole lookup through `match_draft_to_main`'s join once in `run()`, rather than translating an id at each read site: that keeps the scored dicts, the priors the waiver score blends in and the hold-out set from `unavailable_player_ids` all in draft space, and a draft element with no main-game counterpart stays absent, which is the correct degradation.
|
|
709
710
|
- **Deterministic memory before LLM memory.** `league-recap` records every run to an append-only ledger and computes streaks, trends, season phase and the season fine tally from it in Python. The model is handed those as vetted facts through anchored prompt sections and is never asked to remember, infer, or re-derive history — the one conduit per fact is what makes an editorial's historical claims checkable against the store. The fines section makes the rule explicit: season totals may only be quoted from it, never summed by the model out of the current gameweek's fines. It is handed over every week as optional colour while the *printed* table waits for a season milestone -- a table every week is wallpaper, but a sentence every week is what makes a recap read as though it remembers.
|
|
710
711
|
- **No lockfile, and an upper bound is a decision.** The package ships on PyPI, so what a user runs is whatever `pip install fplkit` resolves that day from the ranges in `pyproject.toml`; CI installs the same way, so CI tests what users get. A lockfile cannot change that for anyone installing from PyPI, and wiring one into CI would hide exactly the failure it looks like it prevents — CI green on the pinned version while a fresh install breaks, which is how PuLP 4.0 would have reached `fpl allocate` (#80). The `requirements.lock` that sat at the root until #81 was the worse middle: three runtime dependencies short, generated on macOS for Python 3.12 so a hash-checked install refused on the Linux/3.11 runner, and read by nothing — a signal of pinning that nothing enforced. Protection for users is an upper bound on the few dependencies whose next major is a real prospect and whose surface the code leans on (`httpx<1`, `pydantic<3`, `click<9`, `platformdirs<5`, `pulp<4`), the weekly CI cron re-resolving from scratch so a breaking release is caught with no commit behind it, and nothing else: a bound on a dependency that bumps majors harmlessly is a recurring PR for no protection. `tests/test_dependencies.py` holds the table of bounds in CONTRIBUTING.md to `pyproject.toml` and fails if a lockfile reappears.
|
|
711
712
|
- **One allocation and one merge for the historical sources.** `historical_season_windows()` (`api/historical.py`) is the only place that decides which of the four seasons a profile spans come from Core-Insights (the newest two, all it publishes) and which from vaastav (the two before), and `make_historical_provider()` and `fpl doctor --providers` both read it. `merge_season_histories()` then keeps one row per `(element_code, season)` with Core-Insights outranking vaastav and announces any repeat, so provenance is a decision rather than two window expressions happening to be disjoint: before #101 a widened window would have doubled a season in every per-season mean and taken two of the reliability window's three slots without raising.
|
|
712
713
|
- **Season data frozen in code is surfaced, not guarded.** Every other season-derived artefact rolls or invalidates itself — the historical window rolls at the July cutover, a stale `player_prior.yaml` or `returnee_snapshot.json` is discarded and rebuilt. The calibrated quality ceilings cannot: they are measurements of a completed season's player cohort, and only re-running `scripts/calibrate_quality_ceilings.py --write` against a newly completed season can replace them. So the generated block records `CALIBRATION_SEASON` beside the fingerprint and `fpl doctor` compares it against the newest completed season (#128). It reports, rather than fails, on purpose: anchors from last season are correct for the whole season being played, so the mid-season state is not a defect, and a rollover changes the truth of the block with no commit behind it — a CI guard would have nothing to fire on until someone touched the code, which is exactly the case it is not for.
|
|
713
|
-
- **Who a player *was* is remembered; where he played is re-derived.** A replay resolves every pick against today's bootstrap, so a player transferred or renamed since lands on a past gameweek's row wearing his current club and current name (issue #169). `_carry_recorded_identity` answers that by keeping what the gameweek already recorded, which works only where something *was* recorded — and two cases have nothing: a first capture, and a coarse row being upgraded, since the coarse tier stores headline numbers and no squad at all. The second is the documented workflow, so a mid-season first run plus `--backfill-detail` writes today's clubs across a whole season (issue #177). `services/player_clubs.py` closes the club half from the gameweek itself instead of from a prior row: the live payload records which fixtures each player's club played, a fixture names its two clubs, and the intersection is where he was — settled outright by a double, narrowed to a pair by a single, and paid for with one `element-summary` request only where today's club is absent from that pair, which is proof he moved and the one case the pair cannot resolve. Because it is *derived* rather than remembered, it also supersedes a recorded club, where name and position are only ever carried: a row a first capture or a coarse upgrade wrote holds today's club as fact, so carrying it forward would preserve the mistake permanently — the same earliest-not-winner lesson as #169, applied to a value the gameweek can recompute. Which makes the *precision* of "derived" load-bearing in both directions. A derivation must supersede a record even when it agrees with today's club, or a player who left and came back keeps a stale row forever; and an assumption must never be called a derivation, or today's club overwrites a correctly recorded one — which is why the ambiguous single-fixture case answers nothing rather than answering "he has not moved". Name and position stay carry-forward-only because fixtures say nothing about them, and a blanking club has nothing to intersect; both fall back to the recorded value, then to today's.
|
|
714
|
-
- **
|
|
714
|
+
- **Who a player *was* is remembered; where he played is re-derived.** A replay resolves every pick against today's bootstrap, so a player transferred or renamed since lands on a past gameweek's row wearing his current club and current name (issue #169). `_carry_recorded_identity` answers that by keeping what the gameweek already recorded, which works only where something *was* recorded — and two cases have nothing: a first capture, and a coarse row being upgraded, since the coarse tier stores headline numbers and no squad at all. The second is the documented workflow, so a mid-season first run plus `--backfill-detail` writes today's clubs across a whole season (issue #177). `services/player_clubs.py` closes the club half from the gameweek itself instead of from a prior row: the live payload records which fixtures each player's club played, a fixture names its two clubs, and the intersection is where he was — settled outright by a double, narrowed to a pair by a single, and paid for with one `element-summary` request only where today's club is absent from that pair, which is proof he moved and the one case the pair cannot resolve. Because it is *derived* rather than remembered, it also supersedes a recorded club, where name and position are only ever carried: a row a first capture or a coarse upgrade wrote holds today's club as fact, so carrying it forward would preserve the mistake permanently — the same earliest-not-winner lesson as #169, applied to a value the gameweek can recompute. Which makes the *precision* of "derived" load-bearing in both directions. A derivation must supersede a record even when it agrees with today's club, or a player who left and came back keeps a stale row forever; and an assumption must never be called a derivation, or today's club overwrites a correctly recorded one — which is why the ambiguous single-fixture case answers nothing rather than answering "he has not moved". Name and position stay carry-forward-only because fixtures say nothing about them, and a blanking club has nothing to intersect; both fall back to the recorded value, then to today's. A lost waiver claim names two players the way a move does and a replay restamps both the same way, so it is carried by the same rule — read off the earliest line that recorded any claims rather than the earliest squad line, since a gameweek captured before schema version 6 holds its squad on a line that records nothing about them (issue #332).
|
|
715
|
+
- **Absence from the accepted set is not inactivity.** The draft transaction feed carries every claim the league processed, and a claim a rival won is evidence the manager was active — but the ledger held completed moves alone, so a manager whose whole gameweek was one lost claim was written into the permanent record as having sat it out, and the recap named real people as idle on that basis (issues #329, #332). The row now carries `lost_claims` beside `transactions` rather than a result code inside it: nothing moved, so a lost claim has no points and no net, and folding it into a list every consumer reads as completed moves — the waiver streaks sum `net` over it, the identity carry pairs it slot for slot — would have made each of them learn to skip entries. Only the lost-to-a-rival denial is recorded; the feed's other denial means the manager's own earlier accepted claim had already used the drop, which is the cascade behind a success and never an attempt. The field is three-state on purpose: a list is what the feed held, `[]` included, and `None` says nothing was recorded — a classic row, an unknown row, or a row written before the field existed. That last state is what makes the backfill work without a migration: `coverage()` counts such rows on draft partitions, `_gaps` hands those gameweeks to the detailed tier behind `--backfill-detail`, the replay re-records them from the feed while it still serves the season, and because `[]` differs in content from `None` the superseding line lands once and the target converges. Presence is carried as well as identity: the feed is a log, so a finished gameweek's claims are all in it and a replay that finds fewer has lost some — a player it can no longer place, or a gameweek the feed has stopped serving — and `_carry_recorded_claims` keeps the longest recorded list rather than letting the remnant supersede it, while a longer replayed list still lands as the correction it is.
|
|
716
|
+
- **A row that knows less never supersedes one that knows more.** The ledger is append-only and reads take the newest row per manager, so a capture that writes a null over a recorded value destroys it as surely as an edit would — and permanently. Draft is where that bites: it has no per-manager history endpoint and its standings always describe the current gameweek, so a *replayed* draft gameweek derives no league position and no cumulative total at all. Writing those rows out cost the ledger every position that gameweek held, which is the one input the streak and season-count projections have, and each replay left a full set of dead lines behind (issue #223). Three things stop it. `_carry_recorded_standings` fills a null from the *latest-captured* line that recorded the field — read off the whole file rather than off `resolved_gameweek`'s winner, and ordered by `captured_at` rather than by position in it, since `append_rows` skips on tier rank alone and so does not guarantee the two agree — the same read-past-the-winner move `_carry_recorded_identity` makes, and for the same reason: by the time anyone notices, the degraded row is the winner. What keeps a degraded row out is the value being non-null, not the line being earliest, and that holds whichever way the pass runs; direction only settles which recorded value wins when several disagree, and there the later one is right — a gameweek captured repeatedly while bonus and late results were still landing holds a stale position in its first capture and the settled one in its last (issue #320). Identity stays earliest-first, because a name and a position do not settle over a gameweek the way a table does. `_fill_draft_standings` reconstructs what the ledger can supply on its own, summing a cumulative total from earlier gameweeks and ranking from it (at the league's start gameweek the total *is* the gameweek score, so `gross_points` alone settles that table), and derives a whole table or none of it. Every manager needs a total, because ranking the subset that summed cleanly would renumber everyone below whoever was left out; and no row may already carry a position, because `_assign_cohort_ranks` fills only the nulls, so a cohort part-carried and part-re-derived is ranked against two tables at once and lands two managers on the same place with nobody on the next. Draft breaks a head-to-head tie on points-for, which a cumulative total cannot reproduce, so those two rankings disagree in the ordinary case rather than only on a tie — and a null, which every streak condition holds across, beats a wrong position recorded as fact. `_repair_recorded_standings` then sweeps already-damaged gameweeks, ascending, on every run: it re-fetches nothing, so it needs neither `--backfill-detail` nor a request, and it is the only path that heals a ledger damaged before this existed. Filling only nulls is what makes all three idempotent — a genuine correction still lands, and a replay that reproduces a gameweek exactly now writes no line at all.
|