Audit: 5-reviewer sweep — 19 fixes across every stage, +24 tests

Correctness: review vetoes persist via a dedupe_veto column (resolve
re-runs no longer overturn humans); diff emits second copies whose
confident version matches no owned copy (spec: pairs own only on both
ids) and fetches the live collection with refresh; resolve pairs
titles.json entries to rows by title so a reshoot photo updates
provenance instead of duplicating rows; version lookups survive empty
/thing results; publisher tie-break now honors the mixed
base/expansion veto and refuses multi-candidate picks; empty-normalized
(non-Latin) titles never count as exact.

Upload: LoginError aborts a run instead of logging N bogus failures
(and 3 identical consecutive failures abort as systemic); Cloudflare
interstitials are detected; added-without-version gets its own logged
status that verify understands; same-game updates run one per pass so
the name-targeted row edit can't overwrite a fresh version; absent
diff outputs fail loudly; pagination clicks are paced.

Web review: a lock serializes freshen/decide (threadpool race dropped
decisions); failed saves roll memory back and always alert the browser
(non-JSON 500s included); session warnings reach the page instead of a
StringIO; state-load failures and dead servers show banners instead of
a blank page; duplicate (title, photos) rows are addressable by
ordinal.

Consistency: shared CONFIDENT_VERSION_STATUSES, client_for(),
Config paths for every artifact, one review-port constant, named
matching thresholds, strict collection-id parsing, error-doc responses
never cached, unknown config keys warn, extract reports dropped vision
entries, fixture generators share escaping + marker text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-02 14:10:57 -04:00
parent abf7181475
commit 38e20f2c30
26 changed files with 1003 additions and 249 deletions
+10 -21
View File
@@ -17,6 +17,8 @@ from __future__ import annotations
from pathlib import Path
from fixture_common import esc, write_cache_marker, write_data_marker
from bggpipe.bgg_client import cache_key
TARGETS = (Path("tests/fixtures/bgg_cache"), Path("data/bgg_cache"))
@@ -295,7 +297,7 @@ VERSIONS: dict[int, list[tuple]] = {
def search_xml(results: list[tuple]) -> str:
items = "".join(
f'<item type="{type_}" id="{bgg_id}">'
f'<name type="{name_type}" value="{_esc(name)}"/>'
f'<name type="{name_type}" value="{esc(name)}"/>'
f'<yearpublished value="{year}"/></item>'
for bgg_id, name, year, type_, name_type in results
)
@@ -306,12 +308,12 @@ def stats_xml(things: list[tuple]) -> str:
items = ""
for bgg_id, name, year, type_, owned, rank, publishers in things:
links = "".join(
f'<link type="boardgamepublisher" id="1" value="{_esc(p)}"/>'
f'<link type="boardgamepublisher" id="1" value="{esc(p)}"/>'
for p in publishers
)
items += (
f'<item type="{type_}" id="{bgg_id}">'
f'<name type="primary" value="{_esc(name)}"/>'
f'<name type="primary" value="{esc(name)}"/>'
f'<yearpublished value="{year}"/>{links}'
f"<statistics><ratings>"
f'<owned value="{owned}"/>'
@@ -326,14 +328,14 @@ def versions_xml(bgg_id: int, versions: list[tuple]) -> str:
version_items = ""
for vid, name, year, publishers, languages in versions:
links = "".join(
f'<link type="boardgamepublisher" id="1" value="{_esc(p)}"/>'
f'<link type="boardgamepublisher" id="1" value="{esc(p)}"/>'
for p in publishers
) + "".join(
f'<link type="language" id="1" value="{_esc(lang)}"/>' for lang in languages
f'<link type="language" id="1" value="{esc(lang)}"/>' for lang in languages
)
version_items += (
f'<item type="boardgameversion" id="{vid}">'
f'<name type="primary" value="{_esc(name)}"/>'
f'<name type="primary" value="{esc(name)}"/>'
f'<yearpublished value="{year}"/>{links}</item>'
)
return (
@@ -343,9 +345,6 @@ def versions_xml(bgg_id: int, versions: list[tuple]) -> str:
)
def _esc(text: str) -> str:
return text.replace("&", "&amp;").replace("<", "&lt;").replace('"', "&quot;")
def main() -> None:
files: dict[str, str] = {}
@@ -364,18 +363,8 @@ def main() -> None:
(target / name).write_text(xml)
# provenance marker: anything resolved from this cache is stub-derived
# and NOT upload-ready; re-recording real fixtures removes the marker
(target / "STUB_FIXTURES.marker").write_text(
"This cache contains hand-written stub XML, not real BGG "
"responses. Data resolved from it must not be uploaded.\n"
)
DATA_MARKER.parent.mkdir(parents=True, exist_ok=True)
DATA_MARKER.write_text(
"The CSVs in this directory were resolved from hand-written stub "
"fixtures, not real BGG data — version_ids are SYNTHETIC. The "
"upload stage refuses to run while this file exists. Delete it "
"only after re-resolving against real recorded fixtures "
"(BGG_API_TOKEN + scripts/record_fixtures.py + resolve --force).\n"
)
write_cache_marker(target)
write_data_marker(DATA_MARKER.parent)
print(f"Wrote {len(files)} fixture file(s) to {' and '.join(map(str, TARGETS))}")
print(f"Wrote {DATA_MARKER} (committed; upload refuses while it exists)")