Files
bggpipe/CLAUDE.md
T
Eric WagonerandClaude Fable 5 6ab4a836b4 Audit round 9 lands: the location layer keeps its promises
Five blind reviewers over the day-old shelves layer, ~28 verified
findings — plus Eric's screenshot catching the biggest one live: the
generic .card is flex (built for review's photo-beside-ballot layout),
so unit headers shared a row with their grids and unshelved rows
flowed horizontally off the page. Shelf cards are now .card.stack.

The model fixes. Containment chains resolve recursively with a cycle
guard — minis inside an insert inside a big box live where the big box
does, instead of vanishing from every list; the stored-in endpoint
walks the whole chain when refusing cycles, and clears the newly
contained game's own shelf spot (one box must never consume capacity
in two openings). Contained games are listed residents but occupy no
shelf space: only physical boxes are stacked and fit-checked — the web
report now agrees with the dims report about the same opening. A
location pointing at a vanished opening (hand-edited or reverted
store) SURFACES as unshelved with a "shelf gone" chip in the app and
counts as homeless in the CLI, instead of hiding the game from every
list while the page declares everything has a home.

Honest edges. Opening dimensions are all-or-none everywhere (a
half-sized opening silently became limitless; the CLI report crashed
formatting it); a second grid on a unit continues the row letters so
labels stay unique and label-addressed CSV imports keep working, and
row letters survive past Z; CSV re-imports preserve hand-entered
notes; the ambiguity reject names the fix that actually works;
corrupt furniture/locations stores speak a 500 instead of a raw
traceback; the dims help text stops saying Kallax; DIM_AXES gets one
home in models.py instead of three drifting copies; the new stores
join CLAUDE.md's commit registry.

The page behaves. Custom-dims fields hide unless the custom preset is
chosen (typed values were silently discarded); the sheet is a real
dialog (role, aria-modal, Escape, focus return, one layer at a time);
backdrop close requires press AND release on the backdrop (a text-
selection drag out of the search box no longer dismisses); refresh
goes through changeGate and stops wiping the search mid-interaction;
the prompt() chain is an inline per-unit form with client-side
all-or-none validation; unit-create only toasts success after the
openings actually land, recovers from its own half-failures, and
guards against double-submit (a click retried against the re-rendering
DOM built a second grid — caught live in a Playwright run); warnings
speak (aria-labels on ⚠ and overfull in the fill bar's label);
unmeasured boxes are visible in cells and sheet rows; the library's
unshelved filter matches the Shelves page's definition; reorder
buttons have names; the detail locform wraps at phone width.

Eight new regression tests from the seats' sketches; 380 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-09 14:02:00 -04:00

8.5 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

bggpipe — a resumable, idempotent CLI pipeline that turns shelf photos into a BoardGameGeek collection, in six stages:

# Command What it does Status
1 bggpipe extract Claude vision reads titles + edition cues from photos/ working
2 bggpipe resolve match titles to BGG IDs and versions via XML API2 working (real API data since 2026-08-05)
3 bggpipe review human review of ambiguous/unmatched items; --web serves a FastAPI UI on port 8377 working
4 bggpipe diff diff approved matches against the existing BGG collection working
5 bggpipe upload add games via a logged-in Playwright session working — all browser flows verified live 2026-08-06 (62 adds + 36 version updates landed)
6 bggpipe enrich fetch full game/version metadata into games.json working
+ bggpipe export render the library as self-contained static pages (covers downloaded, never hotlinked; shelf photos never included; Powered-by-BGG badge slot) working
+ bggpipe dims offline shelf-space report over box dimensions enrich collects from BGG versions (dims live per-version, not per-game; 0 = never entered; versionless games need a unanimous chorus within 0.5"/axis else conflicting — never guessed); fit checks run against data/furniture.json openings working
+ bggpipe shelve --import bulk-assign games to openings from a name,opening CSV; rejects (unknown/ambiguous/doesn't-fit/contained) reported, never guessed working

Full design lives in docs/spec.md (read it before changing pipeline semantics); the upload-stage walkthrough is in docs/bgg-upload-flow.md.

Commands

  • uv sync — install deps (Python 3.12+, managed by uv; use uv add, never pip). uv run bggpipe init handles first-run setup (folders, .env credentials, the one-time playwright install chromium).
  • uv run bggpipe web — the app: seven pages (Pipeline /, Photos, Titles, Review, Queue, Library, Help) in a shared sidebar shell (responsive: hamburger nav + stacked tables under 900px); stage runs execute one-at-a-time in a background job. --lan binds 0.0.0.0 behind a per-device access key: persisted in data/.lan_key (gitignored), printed as a QR at startup, cookie-paired for a year, required on EVERY network request (loopback clients and /static/* are exempt; the Host/Origin guard still applies). Phone camera uploads (generic image.jpg names) get minted shelf-<timestamp> names — only explicitly-named files trigger the replace-to-reshoot flow.
  • uv run bggpipe <stage> — run a pipeline stage. Non-secret settings come from config.toml (dirs, rate limit, vision_provider + per-provider [vision.*] blocks — "anthropic" or any OpenAI-compatible endpoint incl. local Ollama); --config overrides the path.
  • uv run pytest — the suite runs fully offline against fixtures. Tests marked live hit the real BGG API (read-only) and are skipped unless you pass --run-live.
  • uv run ruff check / uv run ruff format — lint (rules E, F, I, UP, B, SIM) and format.

Layout

  • src/bggpipe/cli.py (typer app), one module per stage (extract, resolve, review + webreview, diff, upload, enrich); templates/shell.html + templates/pages/* + static/app.{css,js} are the web UI (the stylesheet is the design system — tokens derive from the mascot art), plus bgg_client.py (rate-limited XML API2 client that caches responses to data/bgg_cache/), jobs.py (single-slot background stage runner for the web UI), normalize.py (title normalization), models.py (dataclasses), config.py, fsio.py (atomic writes), init_wizard.py (first-run setup).
  • scripts/write_stub_fixtures.py / write_photo_fixtures.py generate synthetic fixtures; record_fixtures.py re-records real API responses once a token exists.
  • tests/fixtures/bgg_cache/ — recorded real XML fixtures the offline tests run against.
  • data/ — pipeline state (CSV/JSON artifacts are committed; caches are not — see Git).

Hard rules (from spec — never violate)

  • ≤1 request every 2 seconds to any BGG endpoint; jittered backoff on 429/503. Upload stage: 24 s randomized delay between games.
  • Credentials never touch disk or logs. ANTHROPIC_API_KEY, BGG_USERNAME, BGG_PASSWORD, BGG_API_TOKEN come from env vars only. Playwright storage state is credential-adjacent — keep it gitignored.
  • The XML API requires a registered app token (Authorization: Bearer, from BGG_API_TOKEN) — unregistered requests get 401. The token is ACTIVE and tests/fixtures/bgg_cache/ holds real recorded responses; add fixtures for new tests with scripts/record_fixtures.py.
  • Every stage is idempotent and resumable — killing mid-run and restarting must lose no work; re-runs skip already-processed items.
  • Use only the XML API2 and the public website — no undocumented BGG endpoints (BGG tightened access policies in 2025).
  • BGG has no write API: writes drive the real website with a logged-in Playwright session.
  • Synthetic data must never reach upload. The stub era ended 2026-08-05: matches.csv and tests/fixtures/bgg_cache/ now hold real API data. The guard mechanism stays armed: the stub-fixture generators write data/bgg_cache/STUB_FIXTURES.marker (gitignored) and data/STUB_DATA.marker (committed), and the upload stage MUST refuse to run while either exists — regenerating stubs re-locks upload automatically.

Domain gotchas

  • Base game vs. expansion vs. new edition is the top failure mode — bias matching toward ambiguous over auto-match ("Wingspan Europe" must not match base Wingspan).
  • Editions/versions matter: Eric owns multiple editions of some games — each is a separate collection entry (keyed by collid on BGG). Never guess a version: no legible cues → version_unknown and a version-less collection entry.
  • Normalize titles (casefold, strip punctuation/articles, special chars like é/&/:) identically on both sides of a match; dedupe across photos but keep source_photos provenance.
  • Human curation is durable: data/title_splits.json (photo-scoped split-into-copies decisions, honored by extract's dedupe AND resolve's dedupe), data/title_edits.json (corrected reads/cues, applied before dedupe on every titles.json rebuild), data/title_removals.json (lines removed from the catalog — filtered out of every rebuild; delete the record to undo), data/title_additions.json (games added without a photo — joined into every rebuild; a later photo sighting dedupe-merges with them), data/local_games.json + data/local_art/ (hand-written facts and a cover photo for off-BGG games — the ONLY source for them, merged over the photo reads by enrich), and data/furniture.json + data/locations.json (the owner's shelf layout and each game's physical spot — local-only, never BGG's business) persist forever. Row-level decisions persist via the dedupe_veto column — edits never drop veto'd rows (a rename retitles them in place); removal drops them (explicitly discarding the line).
  • RPGs are local-only citizens: when the board-game search runs dry, resolve falls back to type=rpgitem (same geekdo API/token). RPGGeek items carry their OWN link types (rpgdesigner, rpgpublisher, rpggenre, rpgcategory, rpgmechanic) — a board-game-only parser silently returns nothing for them. Matched rpgitems enrich into the library but diff routes them to local_only — they must never reach to_add.csv/upload (their collection lives on RPGGeek, out of scope).
  • Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the bgg-api skill. If the spec's BGG behavior changes, update the bgg-api skill to match — they must not drift.

Git

  • Remote is self-hosted Gitea 1.26 (git.kestrelsnest.social/eric/bggpipe), not GitHubgh CLI does not work here.
  • Commit data/matches.csv, data/to_add.csv, data/to_update.csv, data/upload_log.csv, data/titles.json, data/unidentified.json, data/unidentified_dismissed.json, data/title_splits.json, data/title_edits.json, data/title_removals.json, data/title_additions.json, data/local_games.json, data/local_art/, data/furniture.json, data/locations.json, data/games.json, data/STUB_DATA.marker (while it applies), and the collection snapshot XMLs. Never commit data/bgg_cache/, data/extract_raw/, photos/, data/.lan_key, Playwright storage state, or .env.