FM27 27.0.0+2486695: home-grown club resolution check fails on two independent saves

Open
#53 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
12/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Active
Tech stack
python
Domain
data

Research direction

The failing gate is home_grown_club_refs_resolved in the player reader. Start with the home-grown lookup in src/fmsave/readers/persons.py (around lines 650-660, where uid_by_club_index yields None for missing entries) and the club-table binder in src/fmsave/readers/clubs.py (around lines 398-433, which omits extended, unanchored and rejected rows). Run python -m fmsave validate on a FM27 save to reproduce. Done would be a counts-only diagnostic that separates omitted club rows, absent references and unexpected categories, or a documented explanation of the 0.98 threshold on this build. The issue asks for investigation and does not specify a fix.

Written by the indexing model from the issue text.

Description

What went wrong

With fmsave 0.9.0 on Windows / Python 3.12.10, two independent FM27 single-player saves from build 27.0.0+2486695 fail players() with GateCheckError. In each save, the only failed player-reader check is home_grown_club_refs_resolved; the other 17 applied player checks pass. Neither call returns a player table.

Anonymized save Detected player records Observed resolution rate Required minimum
A 51,917 0.9739630144074952 0.98
B 54,591 0.9761279840019997 0.98

Both have known_build: false, with an explicit registration for this FM27 build. fmsave validate exits with code 3.

Reproduction

The filenames below are placeholders, not attached saves:

python -m fmsave info save-a.fm --json
python -m fmsave validate save-a.fm --json
python -m fmsave validate save-b.fm --json

The same failure occurs with fmsave.open(path, strict=True) followed by save.players(). No checks or thresholds were modified or bypassed.

Additional aggregate diagnostics

The club reader reports ok on both saves, but reports these omitted table-row categories:

Save Accepted clubs Extended rows Unanchored rows Rejected rows
A 20,146 462 4,214 406
B 20,289 462 4,229 409

On both saves, names_resolved, relation_sentinel, second_nation_qualifier, and team_resolved are 1.0. Person-block coverage exceeds 0.9999. These are structural checks, not independent confirmation of field meanings.

In 0.9.0's relation decoder, the home-grown lookup uses uid_by_club_index; missing entries become None. The club-table binder omits the categories above. The reports do not establish whether unresolved references target those omitted rows. Reference totals and unresolved-reference classifications are not exposed in these reports.

Request

Could you investigate club-index coverage and the interpretation of these references on FM27, or suggest a counts-only diagnostic that distinguishes omitted club rows, absent references, and unexpected reference categories? I am not requesting a lower threshold or permission to ignore this failure. I have not established corruption or correctness of attributes, CA/PA, or personality values.

Related context: #31 reports passing validation on other saves of this build. I found no dedicated issue for these positive-but-sub-0.98 results.

Privacy

No saves, screenshots, real names, player/club IDs, player field values, file hashes, or private paths are included. The embedded evidence JSON below contains only anonymized aliases, versions, aggregate counts, and validation checks. No save or player records have been exported for this report.

Full anonymized validation evidence (JSON)
{
  "purpose": "Anonymized validation evidence for a draft issue; not submitted",
  "fmsave_version": "0.9.0",
  "python_version": "3.12.10",
  "os": "Windows",
  "game": "FM27",
  "build": "27.0.0+2486695",
  "known_build_full_reader_set": false,
  "strict_player_reader": true,
  "saves": [
    {
      "alias": "A",
      "player_reader_status": "failed",
      "detected_player_records": 51917,
      "player_checks": [
        {
          "name": "players_minimum",
          "observed": 51917,
          "minimum": 5000,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "person_blocks",
          "observed": 0.9999614769728605,
          "minimum": 0.99,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "names_resolved",
          "observed": 1,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "relation_sentinel",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "second_nation_qualifier",
          "observed": 1,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "handling_above_finishing",
          "observed": 0.15921567116744034,
          "minimum": 0.1,
          "maximum": 0.3,
          "passed": true,
          "applied": true
        },
        {
          "name": "outfield_goalkeeper_block_low",
          "observed": 0.9999354935816114,
          "minimum": 0.8,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "with_natural_position",
          "observed": 1,
          "minimum": 0.97,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "height_in_range",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "height_median",
          "observed": 181,
          "minimum": 170,
          "maximum": 190,
          "passed": true,
          "applied": true
        },
        {
          "name": "age_median",
          "observed": 22,
          "minimum": 20,
          "maximum": 30,
          "passed": true,
          "applied": true
        },
        {
          "name": "aged_in_range",
          "observed": 0.9998266396995088,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "condition_sharpness_in_range",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "join_date_valid",
          "observed": 0.8988770537588844,
          "minimum": 0.05,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "world_not_above_current",
          "observed": 0.9995377236743264,
          "minimum": 0.95,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "home_near_current",
          "observed": 0.9795635341025097,
          "minimum": 0.95,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "team_resolved",
          "observed": 1,
          "minimum": 0.98,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "home_grown_club_refs_resolved",
          "observed": 0.9739630144074952,
          "minimum": 0.98,
          "maximum": null,
          "passed": false,
          "applied": true
        }
      ],
      "club_reader_status": "ok",
      "accepted_club_records": 20146,
      "club_reader_anomalies": {
        "unlinked_affiliate_teams": 0,
        "extended_table_rows": 462,
        "unanchored_table_rows": 4214,
        "rejected_table_rows": 406
      }
    },
    {
      "alias": "B",
      "player_reader_status": "failed",
      "detected_player_records": 54591,
      "player_checks": [
        {
          "name": "players_minimum",
          "observed": 54591,
          "minimum": 5000,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "person_blocks",
          "observed": 0.9999633639244564,
          "minimum": 0.99,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "names_resolved",
          "observed": 1,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "relation_sentinel",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "second_nation_qualifier",
          "observed": 1,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "handling_above_finishing",
          "observed": 0.15892729570808375,
          "minimum": 0.1,
          "maximum": 0.3,
          "passed": true,
          "applied": true
        },
        {
          "name": "outfield_goalkeeper_block_low",
          "observed": 0.999938574938575,
          "minimum": 0.8,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "with_natural_position",
          "observed": 1,
          "minimum": 0.97,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "height_in_range",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "height_median",
          "observed": 181,
          "minimum": 170,
          "maximum": 190,
          "passed": true,
          "applied": true
        },
        {
          "name": "age_median",
          "observed": 22,
          "minimum": 20,
          "maximum": 30,
          "passed": true,
          "applied": true
        },
        {
          "name": "aged_in_range",
          "observed": 0.999816812911026,
          "minimum": 0.995,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "condition_sharpness_in_range",
          "observed": 1,
          "minimum": 0.999,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "join_date_valid",
          "observed": 0.9113773332600612,
          "minimum": 0.05,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "world_not_above_current",
          "observed": 0.9982781044494514,
          "minimum": 0.95,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "home_near_current",
          "observed": 0.9936253228554157,
          "minimum": 0.95,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "team_resolved",
          "observed": 1,
          "minimum": 0.98,
          "maximum": null,
          "passed": true,
          "applied": true
        },
        {
          "name": "home_grown_club_refs_resolved",
          "observed": 0.9761279840019997,
          "minimum": 0.98,
          "maximum": null,
          "passed": false,
          "applied": true
        }
      ],
      "club_reader_status": "ok",
      "accepted_club_records": 20289,
      "club_reader_anomalies": {
        "unlinked_affiliate_teams": 0,
        "extended_table_rows": 462,
        "unanchored_table_rows": 4229,
        "rejected_table_rows": 409
      }
    }
  ]
}
Dominant language
Python
Stars
28
Forks
8
Avg merge
42m
Merged PRs (30d)
33

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from rhiever/fmsave

All issues in rhiever/fmsave

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.