Skip to content

Join-key calculator casts confident no-match votes off OCR noise mis-parsed as a set code (no lexicon check, 85% of 61k no-match votes affected) #370

Description

@WilfordGrimley

Finding (confirmed live, 2026-07-23; surfaced by the orchestrator's discrepancy sampling on cards 77046/31096/14961). local_ocr.py's _SET_CODE_RE = re.compile(r"\b([A-Za-z0-9]{3,5})\b") accepts any 3–5 character alnum token near a parsed collector number as a candidate set code — there is no check anywhere that the token is a real MTG set code. parse_collector_line's own docstring is explicit that this is deliberate ("tolerant extraction, not validation... the validation rail... is what decides trust") — but the validation rail it's referring to (find_matching_candidates/validate_against_candidates) only ever checks the token against this one card's own name-narrowed candidate list, never against the full, real set-code lexicon (CanonicalExpansion.code, ~1,047 real codes).

The result: OCR noise that happens to look like a plausible token — "sew", "ree", "eee" (artwork/border noise, not text) — reads as a genuine (but wrong) set-code attempt. Since it obviously won't match this card's own real candidates (it isn't a real set code at all), validate_against_candidates returns "parsed-but-no-match", which calculate_join_key_verdict treats as genuine whole-candidate-set negative evidence and casts a confident is_no_match=True vote (JOIN_KEY_NO_MATCH_CONFIDENCE=0.6) — indistinguishable from a card where OCR genuinely read a real-but-wrong set code.

Sizing, DB-verified 2026-07-23, read-only:

  • Total stage-d-join-key-v1 is_no_match=True votes: 61,247
  • Of those, current stored collector_line_set_code is not present in the known-set-code lexicon (CanonicalExpansion.code, case-insensitive): 52,349 (85.5%)
  • Blank set_code (the legitimate pre-M15 collector-number-only path — unaffected by this defect): 4,586
  • Real, lexicon-valid set code that simply doesn't match this card's own candidates (working as intended): 4,312
  • Restricting to cards that also carry a deductive-backfill-v1 deduction vote (a potential double-signal-conflict population): 6,772 cards carry both a no-match vote and a deduction vote, of which 5,906 (87.2%) rest on a lexicon-invalid set code. (Note: I could not reproduce the specific "4,768" figure referenced when this was flagged to me under any cohort definition I tried — a truly blank-collector-line-OCR card structurally can never reach a parsed-but-no-match no-match vote in the first place, since that requires a parsed collector number. Flagging this as an open item for reconciliation rather than asserting a number I couldn't verify.)

Ask: validate parsed.set_code against the real set-code lexicon before calculate_join_key_verdict is allowed to treat a "parsed-but-no-match" result as confident negative evidence. A set code that isn't in the lexicon should downgrade to a "no-text"-style (rescannable) abstention, not a confident is_no_match vote — the OCR didn't find a genuine (if wrong) set-code attempt, it found noise. This directly inflates the no-match vote pool with false negatives at ~85% of its current volume, which matters for any downstream consumer treating a no-match vote as trustworthy signal (e.g. review-queue routing, #262's clustering).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions