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>
This commit is contained in:
Eric Wagoner
2026-08-02 19:10:26 -04:00
parent 74fc4fe847
commit 851b34e369
8 changed files with 202 additions and 14 deletions
+14
View File
@@ -405,3 +405,17 @@ def test_rpgitem_rows_are_local_only_never_queued():
assert result.local_only == ["ALICE IS MISSING"]
assert [r["bgg_id"] for r in result.to_add] == ["13"] # RPG never queued
assert result.recognized == 1 # RPGs aren't counted against the queue
def test_split_copies_become_extra_collection_entries():
# three vetoed per-photo copies, one owned: one claims the copy, the
# other two are queued as new entries
base = _match("Wiz-War", "104710")
copies = [
{**base, "source_photos": p, "dedupe_veto": "1"}
for p in ("a.jpg", "b.jpg", "c.jpg")
]
result = compute_diff(copies, [_item(104710, 900)])
assert result.already_owned == ["Wiz-War"]
assert len(result.to_add) == 2
assert all(r["second_copy"] == "1" for r in result.to_add)