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:
@@ -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)
|
||||
|
||||
@@ -469,3 +469,70 @@ def test_corrupt_dismiss_file_is_quarantined_not_fatal(tmp_path):
|
||||
store = DismissStore(path)
|
||||
assert store.keys == set()
|
||||
assert (tmp_path / "dismissed.json.corrupt").exists()
|
||||
|
||||
|
||||
def test_split_row_makes_per_photo_vetoed_copies(tmp_path):
|
||||
from bggpipe.resolve import read_matches
|
||||
|
||||
cfg = _setup(
|
||||
tmp_path,
|
||||
[
|
||||
_row(
|
||||
title_raw="Wiz-War",
|
||||
match_status="approved",
|
||||
bgg_id="104710",
|
||||
bgg_name="Wiz-War",
|
||||
source_photos="a.jpg;b.jpg;c.jpg",
|
||||
version_candidates_json='[{"version_id": 1, "name": "8th"}]',
|
||||
)
|
||||
],
|
||||
)
|
||||
session = ReviewSession(
|
||||
cfg,
|
||||
console=quiet_console(),
|
||||
input_fn=scripted(),
|
||||
client=unauthorized_client(tmp_path),
|
||||
)
|
||||
copies = session.split_row(session.rows[0])
|
||||
saved = read_matches(cfg.matches_path)
|
||||
assert [r["source_photos"] for r in saved] == ["a.jpg", "b.jpg", "c.jpg"]
|
||||
assert all(r["dedupe_veto"] == "1" for r in saved)
|
||||
assert all(r["bgg_id"] == "104710" for r in saved) # match survives
|
||||
# each copy picks its own edition: candidates kept, status reopened
|
||||
assert all(r["version_status"] == "version_ambiguous" for r in saved)
|
||||
assert all(r["version_id"] == "" for r in saved)
|
||||
assert len(copies) == 3
|
||||
|
||||
|
||||
def test_split_copies_survive_resolve_rerun(tmp_path):
|
||||
import json as _json
|
||||
|
||||
from bggpipe.resolve import read_matches, run_resolve
|
||||
|
||||
cfg = _setup(
|
||||
tmp_path,
|
||||
[
|
||||
_row(
|
||||
title_raw="Wiz-War",
|
||||
match_status="approved",
|
||||
bgg_id="104710",
|
||||
source_photos="a.jpg;b.jpg;c.jpg",
|
||||
)
|
||||
],
|
||||
)
|
||||
(cfg.data_dir / "titles.json").write_text(
|
||||
_json.dumps(
|
||||
[{"title_raw": "Wiz-War", "source_photos": ["a.jpg", "b.jpg", "c.jpg"]}]
|
||||
)
|
||||
)
|
||||
session = ReviewSession(
|
||||
cfg,
|
||||
console=quiet_console(),
|
||||
input_fn=scripted(),
|
||||
client=unauthorized_client(tmp_path),
|
||||
)
|
||||
session.split_row(session.rows[0])
|
||||
run_resolve(cfg, client=fixture_client())
|
||||
saved = read_matches(cfg.matches_path)
|
||||
assert len(saved) == 3 # not re-merged, not re-resolved
|
||||
assert [r["source_photos"] for r in saved] == ["a.jpg", "b.jpg", "c.jpg"]
|
||||
|
||||
Reference in New Issue
Block a user