Eric's spec, all nine points. Two committed local-only stores follow
the local_games.json pattern — furniture.json (units of openings with
interior dims; a dimensionless opening is a virtual spot like a travel
case) and locations.json (game key -> opening + note). Shelf layouts
are nobody's data but the owner's; nothing touches upload.
The Shelves page builds furniture without hand-editing JSON — the
acceptance bar (two double-wides above three rows of four cubes, two
bookcases, a travel case) is a TEST, driven entirely through the
endpoints the UI calls. Presets for Kallax/Billy/custom/virtual,
grid creation with A1-style labels, openings editable/deletable/
reorderable. Units render as grids: zone, count, fill bar (stacked
thinnest-axis vs interior height), ⚠ on overfull or any resident that
can't fit. Openings open as a modal — a bottom sheet at phone widths,
search-first with thumb-sized targets for the moving-day loop.
Unshelved games list alongside with one-tap suggestions (only openings
they verifiably fit, with room).
Containment composes: a game stored inside another box inherits its
container's location, rides along in the opening's resident list
(marked), and refuses direct assignment naming its container. The
detail page's where-it-lives card gains the picker (openings grouped
by unit, each labeled fits / doesn't fit / can't verify) plus virtual
notes ("lent to Sarah, June"); the Library list shows a location line,
filters by unit or unshelved, and search matches location text and
zones.
bggpipe dims drops its hardcoded Kallax for the user's actual
furniture: per-opening capacity, overfull and misfit warnings,
unshelved count. bggpipe shelve --import loads a name,opening CSV
(ids or labels), rejecting — never guessing — unknown names, ambiguous
copies, unknown/ambiguous openings, misfits, and contained games.
Ten new tests incl. the acceptance flow, inheritance, CSV rejects,
and a phone-sheet smoke; 372 total.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
8.3 KiB
8.3 KiB
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; useuv add, never pip).uv run bggpipe inithandles first-run setup (folders, .env credentials, the one-timeplaywright 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.--lanbinds 0.0.0.0 behind a per-device access key: persisted indata/.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 (genericimage.jpgnames) get mintedshelf-<timestamp>names — only explicitly-named files trigger the replace-to-reshoot flow.uv run bggpipe <stage>— run a pipeline stage. Non-secret settings come fromconfig.toml(dirs, rate limit,vision_provider+ per-provider[vision.*]blocks — "anthropic" or any OpenAI-compatible endpoint incl. local Ollama);--configoverrides the path.uv run pytest— the suite runs fully offline against fixtures. Tests markedlivehit 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), plusbgg_client.py(rate-limited XML API2 client that caches responses todata/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.pygenerate synthetic fixtures;record_fixtures.pyre-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: 2–4 s randomized delay between games.
- Credentials never touch disk or logs.
ANTHROPIC_API_KEY,BGG_USERNAME,BGG_PASSWORD,BGG_API_TOKENcome from env vars only. Playwright storage state is credential-adjacent — keep it gitignored. - The XML API requires a registered app token (
Authorization: Bearer, fromBGG_API_TOKEN) — unregistered requests get 401. The token is ACTIVE andtests/fixtures/bgg_cache/holds real recorded responses; add fixtures for new tests withscripts/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.csvandtests/fixtures/bgg_cache/now hold real API data. The guard mechanism stays armed: the stub-fixture generators writedata/bgg_cache/STUB_FIXTURES.marker(gitignored) anddata/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
ambiguousover 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
collidon BGG). Never guess a version: no legible cues →version_unknownand 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_photosprovenance. - 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), anddata/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) persist forever. Row-level decisions persist via thededupe_vetocolumn — 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 tolocal_only— they must never reachto_add.csv/upload (their collection lives on RPGGeek, out of scope). - Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the
bgg-apiskill. If the spec's BGG behavior changes, update thebgg-apiskill to match — they must not drift.
Git
- Remote is self-hosted Gitea 1.26 (
git.kestrelsnest.social/eric/bggpipe), not GitHub —ghCLI 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/games.json,data/STUB_DATA.marker(while it applies), and the collection snapshot XMLs. Never commitdata/bgg_cache/,data/extract_raw/,photos/,data/.lan_key, Playwright storage state, or.env.