FM27 27.0.0+2486695: home-grown club resolution check fails on two independent saves
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 12/100
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
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from rhiever/fmsave
-
good first issue help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
good first issue help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
training_man: 27.0.1+2494687 writes a non-zero block trailer, training/mentoring readers fail closedOpen
Difficulty 4/5 1-2 days Newbie friendliness 49/100
rhiever/fmsave#82 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
rhiever/fmsave#77 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 43/100
rhiever/fmsave#75 · 2 comments ·
Maintainers usually reply within 1 day
Similar issues
-
needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
gke-labs/kube-agents#5671 ·
Maintainers usually reply within 1 day
-
docs(auth): Method B step 4 should tell users to tick "Use this client for an AI-powered agent"Opendocumentation next-release v0.9.6
Difficulty 1/5 Under an hour Newbie friendliness 88/100
khuisman/mcp-gee-sweet#1446 ·
Maintainers usually reply within 1 day
-
model:sonnet runtime:claude security size:S
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
FootprintAI/Containarium#3671 ·
Maintainers usually reply within 1 day
-
HolidayBase: reassigning `language` does not re-initialize translations or re-populate holidaysPossibly taken @shikhabhadoria claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
vacanza/holidays#4116 · 1 comment ·
Maintainers usually reply within 1 day
-
bug CLI exec
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day