Commit Graph

35 Commits

Author SHA1 Message Date
Eric Wagoner 86d434a400 Audit round 5 (curation feature): 5 blind reviewers, 14 confirmed fixes
The standing post-feature audit over a7f0cfe. Correctness (data): splits
become photo-scoped store records so splitting one edition no longer
force-splits same-named editions, and renaming a split copy migrates its
protection to the corrected title instead of silently re-merging copies.
Correctness (web): edit scoping now counts siblings by NORMALIZED title
(matching how stored edits apply), same-title-same-photos edits are
refused rather than corrupting the sibling entry, split copies serve
their real per-photo cues to the edit form instead of blanks, and a
split whose row vanished underneath returns 409 instead of a false 200.
Silent failures: replay_titles refuses to rebuild from a PARTIAL raw
cache (fresh clone + one --only extract would have truncated the
committed titles.json); the edit endpoint writes in crash-safe order
(cull, record, replay); corrupt curation stores fail loud naming the
file; retried edits don't double-record. Review-decision durability:
drop_rows never drops dedupe_veto rows — a rename retitles them in
place — and writes through a no-reload path so a concurrent rewrite
can't silently discard the cull. Style: catalog action cells get their
own class (.rowactions' flex display broke table alignment), editor
inputs match the design system and stop overriding the global
focus-visible outline, EditBody's clear-semantics docstring scoped to
cue fields, "nothing to change" derived from the record itself.

Tests: 8 new (photo-scoped splits, veto preservation, photo-narrowed
drops, 409s on both curation endpoints under a running job, partial-raw
replay guard, rename-keeps-protection lifecycle, corrupt-store error,
cue-field editing) and the dead edition_hint key in the edit test now
exercises real cue fields. 259 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-02 20:04:15 -04:00
Eric Wagoner a7f0cfee05 Durable curation: persisted splits + pre-resolve title edits, catalog A→Z
Wiz-War had no split button: can_split required a matches row, but fresh
extractions leave multi-photo titles rowless until resolve runs. Splits
are now a title-level decision persisted in data/title_splits.json,
honored by extract's dedupe and resolve's dedupe on every rebuild, with
the button on any multi-photo line — resolved or not.

Same mechanism carries human corrections: data/title_edits.json stores
fixed misreads and known cues (publisher/edition/year/language), applied
before dedupe on every titles.json rebuild, editable from a new inline
form on every catalog line. An edit drops the title's stale matches rows
so resolve re-queries with the corrected data.

The catalog page now sorts alphabetically (case-insensitive; split
copies stay adjacent) instead of extraction order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
2026-08-02 19:47:08 -04:00
Eric Wagoner 851b34e369 Split into copies: the symmetric partner of the merge veto
Three identical boxes in three photos are indistinguishable from one
box photographed three times, so extract's dedupe folds them into one
entry — correct for overlapping shots, wrong for a shelf holding three
editions of a favorite game. The catalog now offers "split into copies"
on multi-photo rows: the row explodes into one row per photo, each
dedupe_veto-flagged so no future resolve re-merges them, each keeping
its match but reopening its own edition slot (candidates preserved when
present). Resolve's provenance-follow skips split rows (their photo
sets are human-authored), the catalog renders surplus split copies as
their own lines with a "copy" chip, and diff's vetoed-duplicate logic
turns them into the extra collection entries they are.

Applied to the real data: Wiz-War is now three copies across IMG_4502/
4504/4528 — one claims the owned collection entry, two queue as new
second-copy adds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:10:26 -04:00
Eric Wagoner 74fc4fe847 RPGs become local library citizens: identified, enriched, never uploaded
When the board-game search (and truncation heads) runs dry, resolve
falls back to type=rpgitem — the geekdo database is shared, so the same
API, token, cache, and classification machinery apply. Matched rpgitems
flow through review and enrich normally but diff routes them to a
local_only bucket, structurally outside to_add/to_update: their
collections live on RPGGeek, beyond this pipeline's write scope. The
library page gains an All/Board games/RPGs filter and an "RPG · local
only" badge; the catalog tags them too. Fixture generators write blanket
empty rpgitem stubs for every known query (the fallback fires for every
unmatched title) with real synthetic entries for Alice Is Missing.

Data: both Alice rows re-resolved from unmatched to auto rpgitem
matches. First diff since the audit reworks also lands their real-data
consequences: Dungeon! gains its TSR edition update on a versionless
copy the old claim ordering missed, to_add rows carry unioned reshoot
provenance, and the Herbaceous typo row's survivor is now the
correctly-spelled title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:01:09 -04:00
Eric Wagoner 33cbfffbd6 Photo detail page: each photo gets a real page, not a bare image
/photos/view/{name} shows the photo large in the shell, every title
read from it with status chips and BGG matches, its reshoot tickets
with working dismiss, prev/next navigation with arrow keys, position
in the gallery, and a link to the raw full-size file. Gallery and
catalog photo links point here now (review's shots keep linking to the
raw image — zooming spine text is their whole purpose). The status
chip renderer moves to app.js so the catalog and photo pages can't
drift; render_page learns an `active` override so a detail page keeps
its nav section lit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 18:37:31 -04:00
Eric Wagoner 08b741671d Re-audit round 4: 5 blind reviewers over the new surface — 24 fixes, +28 tests
The findings clustered exactly where prediction said: the unreviewed web
layer. The big ones: decisions made while an extract/resolve job runs
are now refused with a 409 (the job's end-of-run rewrite from a
start-of-run snapshot would silently revert them); a cross-origin guard
blocks preflight-free mutations from hostile webpages (bodyless run
triggers, cross-site photo form posts); the JobRunner sets terminal
status in a finally catching BaseException (a greenlet death could
wedge every future run behind 409s) and writes tracebacks into the
visible job log; and a boot token lets clients accept the revision
reset after a server restart instead of freezing forever.

Even the thrice-audited core yielded one HIGH: an unvetoed bare
typo-read sibling of a confident row duplicated its add when the game
wasn't in the collection — diff now treats it as satisfied. Second-copy
adds carry a flag through to_add.csv and the upload log so verify
honestly reports them unverifiable instead of OK. Also: merged_into
chains collapse transitively; diff/enrich treat a BGG queue timeout
like a missing token; enrich prunes orphaned games.json keys; the
wizard shell-quotes .env values and creates the file 0600 from the
first byte; fsio stats the tmp inode before replace and uses unique tmp
names; an explicit missing --config errors; storage state is
owner-only; extract re-extracts corrupt caches, aborts on 3 identical
failures, and exits nonzero when nothing succeeded; torn JSON artifacts
degrade with in-browser warnings instead of 500ing every page; photo
uploads are atomic with cache-invalidation ordered first; the pipeline
page computes `running` before the buttons that depend on it; the
photo dropzone alerts on network failure; and lost-contact banners
clear on recovery everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 18:13:34 -04:00
Eric Wagoner 189c534315 Six-page app: sidebar shell with Juniper's portrait, whole-workflow IA
The two-page dashboard/review split becomes a proper information
architecture: Pipeline (stages + live activity), Photos (drag-and-drop,
gallery with per-photo extraction state, reshoot tickets — photo work
lives with photos), Review (decisions only, keyboard-first), Catalog
(the full title ledger with filtering), Queue (what upload will do and
everything it has done), and Library (the enriched collection browser,
with an honest empty state until real BGG data lands). Pages render
server-side from a shared shell — sidebar rail with the rainbow path
running its edge, live count badges on Photos/Review/Queue, and
Juniper's full portrait finally displayed, with her credit and a
standard third-party trademark attribution beneath it (one notice, not
per-mention symbols — the convention for referring to another party's
mark).

Shared client plumbing moves to static/app.js (escaping contract
documented at the innerHTML sink). New endpoints: /api/photos-list,
/api/queue, /api/library, plus a reshoot count in /api/pipeline.
Screenshot review caught two real bugs: photos-list crashed on
bare-array raw caches, and .DS_Store was listed as a shelf photo —
photo_names() now filters by suffix everywhere, including the /photos
allowlist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:34:17 -04:00
Eric Wagoner d3cf0c6151 Web UI: navigation, Juniper-derived design system, accessibility floor
One shared stylesheet (static/app.css) replaces the two ad-hoc style
blocks, with tokens drawn from the mascot drawing: sky background,
cream game-board cards inside confident outlines with flat offset
shadows, hair-purple for brand and actions, shirt-green for go,
bow-tie orange for danger, pipe-fitting gold for trim, jeans navy for
chrome — and the rainbow game path as a stripe under the header, the
one loud element. Accent colors split into object and *-ink variants
so text on light surfaces holds AA contrast.

Navigation: both pages share a header with brand-home link, a Primary
nav with aria-current, and tally counts that link to the lists they
count (dashboard tallies deep-link into the review catalog; review
tallies jump to their sections). Accessibility: skip link, landmark
nav, polite live region for stage activity, status role on banners,
the dropzone is a real button, candidate rows are focusable and
activate with Enter/Space, focus-visible ring throughout, reduced
motion respected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:21:14 -04:00
Eric Wagoner 6ecdd43ed2 bggpipe web: the whole pipeline as a local web app
A dashboard at / joins the review page (now at /review): drag-and-drop
photo upload (re-uploading a photo drops its raw cache so extract
re-reads it), per-stage status cards fed by /api/pipeline (counts and
key NAMES only — never values), and run buttons that execute stages
one-at-a-time in a background JobRunner with captured output streamed
to the page. The real upload sits behind a confirmation, defaults to
dry-run at the API layer, and stays disabled while stub data is
present. The CLI is unchanged and shares all state with the web UI.
python-multipart joins the deps for the upload endpoint; RunBody lives
at module scope because postponed annotations keep FastAPI from
resolving function-local models.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:08:00 -04:00
Eric Wagoner bf9795235a bggpipe init: guided, idempotent first-run setup
One command replaces the clone-era checklist: creates photos/ and
data/, writes a default config.toml, prompts for the four credentials
with hidden input (appended to a 0600 .env, only the missing ones,
values never echoed), and offers the one-time Chromium download.
Re-runs report status and fill gaps; without a TTY it reports instead
of hanging. Groundwork for any future publishing path — PyPI or a
bundled app both need exactly this wizard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:56:28 -04:00
Eric Wagoner 10f65d8aba Credibility pass: comments state constraints, not development history
A skeptical-cloner review flagged the patterns that read as AI-iteration
residue: test comments and section headers narrating the review process
that produced them, "legacy format" framing in a days-old repo, shadow
re-imports appended without reading file headers, one genuine machine
leftover (FIXTURE_CACHE = FIXTURE_CACHE), and a few register slips.
Every history-narrating comment is rewritten as the timeless invariant
it was guarding, test sections are grouped by behavior, function-local
imports are hoisted, and the README loses its one marketing clause and
heaviest dash runs. No behavior changes; 176 tests unchanged and green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 15:08:22 -04:00
Eric Wagoner 92aaa91a49 Re-audit round 3: 5 blind reviewers, 15 fixes, +18 tests — converging
Round 3's two HIGHs: _fill_version resolved versions with the LAST
same-title entry's cues (photo-aware lookup existed since round 1 but
this caller never used it), and the round-2 diff rework let an earlier
row's disagreement consume the exact-version copy a later row matched.
Diff claims now settle strongest-first across all rows (exact matches,
then versionless upgrades, then disagreement/second-copy), unvetoed
bare duplicates stay owned per spec, and updates are withheld with a
manual-fix note whenever any copy of the game already carries a version
(the row edit targets by name and could hit the wrong copy).

Also: entry-to-row pairing matches by photo overlap before position
(titles.json order churn from reshoot filenames could swap editions);
BGGQueueTimeout defers a title like a missing token; DismissStore
writes atomically, mutates memory only after the write, and
quarantines a torn file instead of bricking the server; version-picker
page-limit exhaustion stays retryable; verify's copy-count shortfall
reports once per game (the old guard was dead code); the upload log
header is created atomically; transient version-lookup failures record
a retryable version_error, not terminal version_unknown; extract
isolates per-photo failures and salvages JSON followed by prose; a
state revision counter stops stale poll responses reverting decisions;
plus the shared-predicate/fsio/docstring consolidation and CLI wiring,
live-diff, verify-wiring, and search-guard tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:55:04 -04:00
Eric Wagoner 65d4cdd5ec Re-audit round 2: 5 blind reviewers, 17 fixes, +12 tests
The re-run confirmed round 1 held and then caught second-order bugs in
its own fixes plus two long-standing ones everyone missed. TUI decisions
after a mid-session reload were counted but never written (rows are now
re-adopted into the fresh list on every save, preferring undecided slots
on duplicate keys); row_ix was computed by equality so duplicate rows
shared an ordinal (identity now, merges included, veto sends it); upload
job keys collided for two same-version copies (completions are counted
per key, so --limit or an interrupt can no longer strand the second
copy); diff consumes collids on exact-version matches (a vetoed
same-version second copy was silently swallowed) and splits mismatches:
report-only disagreement while an unclaimed copy exists, second-copy add
only when every copy is claimed.

Also: XML responses are validated and written atomically before caching
(a torn or truncated 200 body can never poison a re-run), JSON artifacts
write atomically, thing/search parsers refuse missing ids like the
collection parser, empty game names are refused by the upload queue, a
never-rendering version picker fails retryably instead of terminally,
the systemic-failure abort compares exception types, blocked same-title
entries defer as a group so positional pairing can't misalign,
truncation heads pick the earliest separator, diff messages tell the
truth when a token exists without a username, and the shared-constant
sweep now actually covers every module (statuses, search types, marker
names, client_for, ports). pydantic declared as a direct dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 14:34:44 -04:00
Eric Wagoner 38e20f2c30 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>
2026-08-02 14:10:57 -04:00
Eric Wagoner abf7181475 Username lives in .env only: drop bgg_username from config.toml
load_config no longer reads a bgg_username toml key — BGG_USERNAME in
the environment is its single home (public, but the account should have
exactly one). Docs, messages, and tests updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:36:22 -04:00
Eric Wagoner 8e3cdc145f Docs for strangers: real quick start, own-shelves guide, clone-safe stub guard
README rewritten for a fresh clone: working status, quick start with
env-var table and per-stage commands, photo-taking guidance, a
bring-your-own-shelves section (this repo carries the author's live
data), tokenless workflows, and a dev section. New committed
data/STUB_DATA.marker closes a real gap: the cache marker is
gitignored, so a fresh clone had stub-derived to_add.csv with no
guard — upload now refuses on either marker. Username docs: BGG_USERNAME
in .env is the one place it's needed; config.toml is a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:33:52 -04:00
Eric Wagoner 02a7ac262c Review web UI: live-follow data files, --dev code reload
ReviewSession re-reads matches.csv/titles.json on mtime change so
external extract/resolve runs show up per-request (and stale in-memory
rows can no longer overwrite them); the page polls state every 3s,
re-rendering only on change and never mid-typing. --web --dev adds
uvicorn source-watch restarts, scoped to the package dir so decision
writes to data/ don't trigger them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:25:46 -04:00
Eric Wagoner 96637cfa7f Extract IMG_4566: Alice Is Missing base + Silent Falls expansion
Both resolve as unmatched by design — it's an RPG (geekdo rpgitem), so
the boardgame search realistically returns nothing; empty-search stubs
cover all five queries incl. truncation heads. Resolve also merged six
duplicate reads across photos (Hebarceos -> Herbaceous et al.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:17:50 -04:00
Eric Wagoner 3ca7e7f650 Stage 5 upload: Playwright-driven adds and version updates
Queue from to_add/to_update minus upload_log.csv (append-per-attempt,
so runs resume); per-game failure isolation with 2-4s pacing;
--dry-run/--verify/--retry-failed/--limit; stub-fixture marker blocks
real runs, dry-run warns. Headed browser by default: live recon showed
Cloudflare Turnstile hard-blocks headless, and BGG never reaches
networkidle. Login selectors verified anonymously; version-picker
pagination and the collection-row update flow remain unverified until
real data exists. Client collection fetches gain a refresh passthrough
so --verify sees the live collection, not cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 19:03:25 -04:00
Eric Wagoner 824719edc7 Mascot: Juniper's bggpipe piper joins the header, favicon, and README
Original art (a bagpiper whose bag is a board game box) lives in
assets/; web-sized derivatives ship in the package: a face-crop avatar
in the header and favicon, and the framed full-length piper on the
review done screen. Served via an allowlisted /static route. README
leads with the full portrait, credited to Juniper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 17:35:53 -04:00
Eric Wagoner 8420a0a1ca Post-resolve dedupe: duplicate reads merge, review can veto
Rows resolving to the same (bgg_id, version_id — or both version-
unknown) are the same physical game read twice unless their extraction
cues conflict (two editions stay separate). The survivor is the read
whose transcription matches the BGG name; losers are marked
match_status=merged with a new merged_into column — no row is ever
deleted, and older matches.csv files without the column still read.
Downstream: diff skips merged rows but folds their photos into the
survivor's to_add provenance; enrich and the review passes ignore them.
The web UI gains a Merges section ("Jokin Ha... merged into Joking
Hazard") with a veto (v key) that restores the row as a distinct
approved match, plus a merged catalog chip and header tally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:59:47 -04:00
Eric Wagoner 6da499d31a Review UI: surface the unresolved backlog and a full catalog ledger
The done screen claimed "diff-ready" while 104 extracted titles had
never been resolved (they're invisible to matches.csv until the BGG
token arrives). The state now counts titles.json entries with no
matches row: the header tally shows "awaiting resolve", the done screen
says "Resolved set fully reviewed" with the real extracted total and
what to run on token day, and a read-only Catalog section lists every
extracted title with its status chip (auto/approved/rejected/awaiting
BGG), matched game, version, and source photos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:45:57 -04:00
Eric Wagoner 2273ac7cf7 Web review UI: bggpipe review --web (FastAPI, localhost, no build step)
One self-contained page (inline CSS/JS, system fonts, works offline):
match cards show source photos, extracted cues, and candidates with
cached-XML thumbnails (placeholder tiles until real fixtures exist);
actions are pick / manual BGG id / reject, plus a skippable editions
pass (pick or unknown). Keyboard-first: j/k navigate, 1-9 pick, r
reject, m manual, u unknown, d dismiss. Every decision writes
matches.csv through the same ReviewSession methods the TUI now shares —
the TUI remains as the no-flag fallback. unidentified.json renders as
visually distinct reshoot work-orders with dismissals persisted in
data/unidentified_dismissed.json (survives extract rebuilds). Progress
tally and a diff-ready done screen; photo serving is allowlisted to
photos/ contents; server binds 127.0.0.1 only. Layout leaves room for
a later games.json browse view.

Provenance guard: fixture generators now write STUB_FIXTURES.marker
into their cache dirs, and CLAUDE.md gains the hard rule that stub-
resolved version_ids are placeholders — upload must refuse to run
while data/bgg_cache/STUB_FIXTURES.marker exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:13:29 -04:00
Eric Wagoner 4af83d3626 Enrich stage: full game + version metadata into games.json
Batched /thing?stats=1 fetches (20 ids, sorted so batch cache keys stay
stable), parsing the full frontend-seed payload: designers, artists,
publishers, player counts with Best-majority poll analysis, playtimes,
min age, weight, rating, rank, categories, mechanics, description, and
image URLs. Chosen-version details are reused from matches.csv's stored
candidates — zero extra API calls. Already-enriched keys are skipped
entirely; --refresh bypasses the cache read since ranks and ratings
drift. Degrades gracefully without BGG_API_TOKEN: cached ids enrich,
the rest wait, everything fetched is saved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 14:37:56 -04:00
Eric Wagoner aed969856b Resolve degrades gracefully when BGG_API_TOKEN is missing
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>
2026-08-01 14:35:30 -04:00
Eric Wagoner 4bf7481f9b Extract reports unidentifiable boxes for human retakes
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>
2026-08-01 14:24:16 -04:00
Eric Wagoner 4a466f2a68 Progressive title truncation, publisher tiebreak, thematic-year fix
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>
2026-08-01 14:14:15 -04:00
Eric Wagoner 09274e039c Photo-title fixture set + resolve baseline; language-only cues skip fetch
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>
2026-08-01 14:09:13 -04:00
Eric Wagoner 3543005236 Diff stage: snapshot/live collection modes, to_add + to_update outputs
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>
2026-08-01 14:06:23 -04:00
Eric Wagoner 4f2ae5f525 Real extraction from shelf photos; test title list moves to tests/data
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>
2026-08-01 13:16:43 -04:00
Eric Wagoner 9897da7f82 Review stage: resumable rich TUI for matches and versions
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>
2026-08-01 13:13:20 -04:00
Eric Wagoner b7a1ef8549 Extract stage: vision title + edition-cue extraction, offline-tested
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>
2026-08-01 13:10:13 -04:00
Eric Wagoner 2109e3544a Resolve stage: matching, version resolution, fixtures; BGG API auth
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>
2026-08-01 12:33:32 -04:00
Eric Wagoner 4e1211feb6 BGG API client: disk cache, rate limiting, 202-queue retry, XML parsing
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>
2026-08-01 12:26:03 -04:00
Eric Wagoner d58568cceb Scaffold bggpipe: typer CLI, config loading, title normalization
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>
2026-08-01 12:23:00 -04:00