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:
@@ -0,0 +1,36 @@
|
||||
"""Shared plumbing for the two stub-fixture generators.
|
||||
|
||||
Both write into the same cache dirs, so they must agree on XML escaping
|
||||
(a title containing & or " must not produce malformed XML) and on the
|
||||
provenance-marker text the upload guard depends on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
CACHE_MARKER_TEXT = (
|
||||
"This cache contains hand-written stub XML, not real BGG "
|
||||
"responses. Data resolved from it must not be uploaded.\n"
|
||||
)
|
||||
DATA_MARKER_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"
|
||||
)
|
||||
|
||||
|
||||
def esc(text: str) -> str:
|
||||
"""Minimal XML attribute/text escaping for hand-built fixture strings."""
|
||||
return str(text).replace("&", "&").replace("<", "<").replace('"', """)
|
||||
|
||||
|
||||
def write_cache_marker(target: Path) -> None:
|
||||
(target / "STUB_FIXTURES.marker").write_text(CACHE_MARKER_TEXT)
|
||||
|
||||
|
||||
def write_data_marker(data_dir: Path = Path("data")) -> None:
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
(data_dir / "STUB_DATA.marker").write_text(DATA_MARKER_TEXT)
|
||||
@@ -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("&", "&").replace("<", "<").replace('"', """)
|
||||
|
||||
|
||||
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)")
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_common import esc, write_cache_marker
|
||||
|
||||
from bggpipe.bgg_client import cache_key
|
||||
|
||||
FIXTURE_CACHE = Path("tests/fixtures/bgg_cache")
|
||||
@@ -24,7 +26,7 @@ def search_item(bgg_id: int, name: str, year: int | None, type_: str) -> str:
|
||||
year_xml = f'<yearpublished value="{year}"/>' if year else ""
|
||||
return (
|
||||
f'<item type="{type_}" id="{bgg_id}">'
|
||||
f'<name type="primary" value="{name}"/>{year_xml}</item>'
|
||||
f'<name type="primary" value="{esc(name)}"/>{year_xml}</item>'
|
||||
)
|
||||
|
||||
|
||||
@@ -104,10 +106,7 @@ THINGS = {
|
||||
|
||||
def main() -> None:
|
||||
FIXTURE_CACHE.mkdir(parents=True, exist_ok=True)
|
||||
(FIXTURE_CACHE / "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"
|
||||
)
|
||||
write_cache_marker(FIXTURE_CACHE)
|
||||
for query, items in SEARCHES.items():
|
||||
key = cache_key("search", {"query": query, "type": SEARCH_TYPES})
|
||||
total = items.count("<item ")
|
||||
|
||||
Reference in New Issue
Block a user