Skip to contents

An attacker's candidate list is a **set**. If (ANON 1, RAW 2) appears twice, RAW 2 is still one guess, and a uniform draw among the tied best candidates must still give it probability `1 / (number of distinct candidates)`. Both tie paths in this package count *rows* instead: `resolve_min_distance_ties()` shuffles every row of the tie group, and `reid_per_anon()` computes `sum(v == true_score)` over rows. A duplicated wrong candidate therefore takes a share of the draw it is not entitled to, and the reported risk falls.

Usage

validate_unique_candidate_pairs(scores, fn_name)

Arguments

scores

a score table, already through [validate_reid_scores()]

fn_name

calling function, for the message

Value

`scores`, invisibly

Details

The reason this needs an explicit guard rather than being caught downstream is Issue #60: the analytic value and the simulated value are wrong **in the same direction and by the same amount**, because they read the same inflated multiset. The random baseline is `1 / N_CANDIDATES`, which is inflated identically, so `lift` does not move either. The "would I notice if this broke" cross-check that `docs/lessons-learned.md` section 2 asks for is precisely what fails here, and it fails silently, downwards.

`match_optimal()` and `combine_scores()` already refused duplicated input. This is the same test, so that `match_greedy()` and `reid_evaluate()` – the two entry points that were still permissive – hold to the same contract.