soulsync/tests/matching/__init__.py
Broque Thomas 235ada7e0f Add pure artist-name comparison helper with alias awareness
Issue #442 — files tagged with one spelling of an artist's name
(Japanese kanji `澤野弘之`) get quarantined when SoulSync expects the
romanized spelling (`Hiroyuki Sawano`). Raw similarity comparison
scored 0% across scripts. MusicBrainz exposes alternate-spelling
aliases on every artist record but the verifier never consulted
them.

This commit adds the pure helper that does the alias-aware
comparison. No I/O, no DB access, no network. Caller supplies the
aliases (looked up from library DB or live MB by later commits in
this PR). Default threshold matches the verifier's existing
`ARTIST_MATCH_THRESHOLD` (0.6) so wiring this in preserves current
pass/fail semantics on the no-alias path.

# API

```
artist_names_match(expected, actual, *, aliases=None,
                   threshold=0.6, similarity=None)
    -> (matched: bool, best_score: float)
```

- Direct compare first (fast path + baseline score)
- If below threshold, score each alias against `actual`
- First alias to clear threshold → match
- Returns the best score across all candidates so callers can log
  the score they made the decision on

```
best_alias_match(expected, actual, aliases=None, *, similarity=None)
    -> (winner: Optional[str], best_score: float)
```

Companion helper for callers that want to surface WHICH alias
triggered the match (debug logs, UI explanations). No threshold —
purely informative.

# Architectural choices

- **Pure function**: no I/O. Caller (verifier, future matching-engine
  consumers) owns alias lookup strategy + threshold tuning.
- **Custom similarity callable**: lets the verifier pass its
  parenthetical-stripping normaliser without this module having to
  know about it. Defaults to lowercase + SequenceMatcher (matches
  the verifier's existing behaviour).
- **Defensive coercion**: aliases input handles None entries, empty
  strings, non-string types, sets, tuples, lists — caller may feed
  raw MB response data without cleaning first.
- **Backward compat**: `aliases=None` or empty → behaves identically
  to a plain similarity check. Paths not yet wired up to alias lookup
  see no behaviour change.

# Tests (28)

- Direct compare (no aliases): exact / case / whitespace / fuzzy /
  different
- Cross-script with aliases: Japanese ↔ romanized (reporter's case 1),
  Cyrillic ↔ Latin (reporter's case 2), symmetric direction, no-match
  fallthrough so aliases don't mask genuine mismatches
- Aliases input handling: None, empty, set, tuple, None-entries,
  non-string entries
- Threshold: default matches verifier's 0.6, custom stricter, custom
  looser
- Custom similarity: applies to both direct + alias compare
- Best-alias-match introspection
- Backward compat parametrised across 5 cases

# What this commit does NOT do

This is the helper module + tests only. Subsequent commits in this
PR populate aliases (MB worker), provide live MB lookup with cache
for un-enriched artists, and wire the helper into the AcoustID
verifier where the quarantine decision actually fires.
2026-05-10 16:08:38 -07:00

0 lines
Python