A 401 on an uncached title no longer aborts the run: cached titles
resolve and save, blocked titles are listed with registration/token
instructions and left out of matches.csv so a future run picks them
up untouched. Supports the take-photos-now, resolve-later workflow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vision prompt now returns {titles, unidentified}: boxes that look
like games but can't be confidently titled are reported (location
relative to identified neighbors, partial text, art notes) instead of
silently omitted. They land in data/unidentified.json keyed by photo,
and the end-of-run summary lists them — plus low-confidence reads —
with instructions to retake a closer photo and re-run. New --force flag
re-extracts everything; pre-feature raw caches (bare arrays) still
parse. Live run on IMG_4499 confirmed the flow and the low-confidence
list correctly flags the known "Hebarceos" misread.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Long transcribed box titles that defeat search now retry with shorter
heads (pre-separator, pre-"Game ..." descriptor, first-two-words from
the pre-subtitle part) matched exact-only against the head — fuzzy
thresholds stay untouched. Tie-breaks gain a publisher pick: when the
box showed a publisher and exactly one exact-named candidate is from
that publisher, it wins (SPI's Sorcerer 1975 now beats the more-owned
White Wizard Sorcerer 2019). The extract prompt excludes thematic/
subject years from year_hint; re-extracting Flat Top's photo drops the
bogus 1942, and a regression test pins that a wrong year can never
drive version selection. Re-extraction also drifted two transcriptions
(DUNGEON!, and Herbaceous misread as "Hebarceos") — fixtures added; the
misread demos review's re-search rescue in the end-to-end run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Data-driven stub XML for all 27 extracted titles (searches incl. empty
results for the long Avalon Hill/SPI box titles, truncated-head retry
queries for later, tie-break stats with publisher links, version lists
aligned with the real collection objectids). Language-only cues no
longer trigger a versions fetch — language can't reach the plausibility
threshold alone, so the request would always be wasted; it still scores
when stronger cues exist. Baseline resolve: 22 auto, 1 ambiguous
(Wiz-War editions), 4 unmatched (the long box titles — next commit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
compute_diff is a pure function over matches + collection items: new
games land in to_add.csv (carrying a confident version when matching
produced one); owned version-less entries with version_auto/approved
matches produce additive to_update.csv rows keyed by collid, consuming
distinct collids when photos show two editions; entries that already
carry a version are never touched — disagreements are reported in the
summary. Unseen collection entries are listed informationally. Live API
mode activates when BGG_API_TOKEN + username exist; otherwise the two
hand-pulled snapshot XMLs (real 2018 collection, 79 unique copies after
collid dedupe) are used, and they double as parsing fixtures in tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live vision run: 4 shelf photos -> 27 unique titles with edition
cues (publishers, Bookcase Game/Designer's Edition wording, art notes);
cross-photo dedupe merged repeat sightings. data/titles.json now holds
real pipeline data, so the hand-typed resolve test list moves to
tests/data/titles.json (tests and record_fixtures updated), and the
matches.csv generated from stub fixtures for that list is removed —
the real resolve run will regenerate it once the BGG token arrives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prompt loop over ambiguous/unmatched rows: pick a candidate (table with
owned/rank), skip, reject, enter a manual BGG id, or free-text re-search
via the cached client. Approvals attempt version resolution from the
title's edition cues, degrading to version_unknown when the API is
unreachable (no token yet). Optional, skippable version pass for
version_ambiguous rows. Every decision rewrites matches.csv atomically,
so q/Ctrl-C/EOF mid-session loses nothing. Tests drive the loop with
scripted input against a synthetic matches.csv and the fixture cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-photo raw results cached under data/extract_raw/ (gitignored) so
re-runs are free, --only re-extracts a single photo, and titles.json is
rebuilt with dedupe that keeps conflicting-edition sightings separate.
HEIC converts via pillow-heif; images downscale to <=1568px long edge;
model JSON parsed defensively (code fences, surrounding prose). Vision
callable is injectable — tests use a local fake; the real one uses the
anthropic SDK (approved) with the model from config.toml.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bggpipe resolve works end to end: search -> exact/fuzzy candidate
scoring -> auto/ambiguous/unmatched classification with owned-count
tie-breaks (mixed base/expansion candidates never auto-match), version
scoring from edition cues (never guessed; no cues -> version_unknown),
idempotent matches.csv appends.
Discovered mid-build: BGG now requires registered-application Bearer
tokens on the XML API (2025 policy change) and returns 401 otherwise.
Client sends Authorization from BGG_API_TOKEN and raises an actionable
BGGAuthError; CLAUDE.md and the bgg-api skill are updated to match.
Live fixture recording is blocked until registration is approved, so
tests replay hand-crafted stub fixtures via a network-refusing
transport; scripts/record_fixtures.py re-records real XML under the
same cache keys once a token exists. One live read-only smoke test is
skipped unless --run-live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
httpx client with injectable clock/sleep/rng for testability. Successful
responses cached under data/bgg_cache/ keyed by endpoint+params; 202
retries follow the spec schedule (2/5/10/30s, give up after 5); 429/503
get jittered exponential backoff; consecutive requests are spaced
rate_limit_seconds apart. Parsers (via defusedxml, per security hook)
cover search, thing (+stats/+versions), and collection, including the
Not Ranked and error-document cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
uv project with typer/httpx/rapidfuzz, ruff and pytest wired into
pyproject. Six stub subcommands matching the pipeline stages, config.toml
plus BGG_USERNAME env override, accent/ampersand/article-safe title
normalization with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>