The real-data era: token live, stubs retired, editions on demand

BGG application approved. The migration the stub markers guarded for
weeks: both synthetic caches deleted; tests/fixtures/bgg_cache
re-recorded from the live API (recording list extended to every
scenario the suite exercises — Civilization truncation, the Sorcerer
SPI tiebreak, StarForce, Flat Top's thematic year, Alice Is Missing's
rpgitem fallback); resolve --force re-matched all 133 titles for real
(109 auto, 6 ambiguous, 18 unmatched, 30 edition ballots);
data/STUB_DATA.marker deleted with its exit condition met — the guard
mechanism stays armed should stubs ever regenerate.

Reality fixed one bug and taught one lesson. The bug: a multi-type
search lists an expansion twice (once per matched type) and the parser
kept the generic boardgame entry — parse_search now dedupes by id
preferring the specific type, which is what keeps expansion tagging
(the base-vs-expansion review guard) alive on real data. The lesson:
hand-built ambiguity is tidier than the real thing — Wingspan has 46
versions with three plausible English Stonemaier printings, so the
suite's synthetic version ids and version_auto expectations became
real ballots (assertions updated to recorded reality; the cue-plumbing
test keeps its crafted two-version scenario via an injected
transport).

New: pick edition. A cue-less matched row is version_unknown by design
(never guess) — but the owner knows which printing the box is.
open_version_ballot() fetches the game's complete version list,
cue-scores it when cues exist, and marks the row version_ambiguous so
the normal Review edition pass presents it; the Titles page grows the
button (Eric's three Wiz-Wars: two cue-less copies can now each claim
their edition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-05 18:50:17 -04:00
co-authored by Claude Fable 5
parent 15d3120029
commit 59f4b8c43c
135 changed files with 15851 additions and 266 deletions
+65 -6
View File
@@ -201,15 +201,17 @@ def test_unmatched_research_from_fixture_cache_with_version(tmp_path):
run_review(
cfg,
console=quiet_console(),
input_fn=scripted("f Wingspan", "1"),
# the real search lists a fan pack first; Wingspan proper is #2
input_fn=scripted("f Wingspan", "2"),
client=fixture_client(),
)
(row,) = read_matches(cfg.matches_path)
assert row["match_status"] == "approved"
assert row["bgg_id"] == "266192"
# cues + fixture versions -> resolved to the 2019 Stonemaier English edition
assert row["version_status"] == "version_auto"
assert row["version_id"] == "465063"
# cues + the real version list -> a handful of English Stonemaier
# printings stay plausible; the edition pass gets the ballot
assert row["version_status"] == "version_ambiguous"
assert "433233" in row["version_candidates_json"]
def test_resumable_quit_midway_then_continue(tmp_path):
@@ -440,14 +442,36 @@ def test_fill_version_uses_the_approved_rows_own_cues(tmp_path):
],
)
(cfg.data_dir / "titles.json").write_text(json.dumps([entry_good, entry_bad]))
two_versions = """<items><item type="boardgame" id="266192">
<name type="primary" value="Wingspan"/><yearpublished value="2019"/>
<versions>
<item type="boardgameversion" id="111">
<name type="primary" value="English first edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
<link type="language" id="2184" value="English"/>
</item>
<item type="boardgameversion" id="222">
<name type="primary" value="German edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="22160" value="Feuerland Spiele"/>
<link type="language" id="2188" value="German"/>
</item>
</versions></item></items>"""
crafted = BGGClient(
cache_dir=tmp_path / "crafted_cache",
transport=httpx.MockTransport(
lambda req: httpx.Response(200, text=two_versions)
),
)
session = ReviewSession(
cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
cfg, console=quiet_console(), input_fn=scripted(), client=crafted
)
row = session.rows[0]
session.decide_pick(row, {"bgg_id": 266192, "name": "Wingspan", "year": 2019})
# the row's own photo (good.jpg) must select entry_good's cues
assert row["version_status"] == "version_auto"
assert row["version_id"] == "465063"
assert row["version_id"] == "111"
def test_dismiss_failure_keeps_ticket_visible(tmp_path, monkeypatch):
@@ -534,3 +558,38 @@ def test_split_copies_survive_resolve_rerun(tmp_path):
saved = read_matches(cfg.matches_path)
assert len(saved) == 3 # not re-merged, not re-resolved
assert [r["source_photos"] for r in saved] == ["a.jpg", "b.jpg", "c.jpg"]
def test_open_version_ballot_puts_every_edition_up_for_review(tmp_path):
# a cue-less row is version_unknown by design — but the human knows
# which printing the box is; the ballot fetches the FULL version list
cfg = _setup(
tmp_path,
[
_row(
title_raw="Wingspan",
match_status="auto",
bgg_id="266192",
bgg_name="Wingspan",
version_status="version_unknown",
)
],
)
session = ReviewSession(
cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
)
row = session.rows[0]
count = session.open_version_ballot(row)
assert count == 46 # the real recorded version list, complete
assert row["version_status"] == "version_ambiguous"
candidates = json.loads(row["version_candidates_json"])
assert {c["version_id"] for c in candidates} >= {433233}
saved = read_matches(cfg.matches_path)[0]
assert saved["version_status"] == "version_ambiguous" # persisted
unmatched = _setup(tmp_path / "second", [_row(title_raw="Mystery")])
session2 = ReviewSession(
unmatched, console=quiet_console(), input_fn=scripted(), client=fixture_client()
)
with pytest.raises(ValueError, match="no BGG match"):
session2.open_version_ballot(session2.rows[0])