feat: exercise_aliases table + lookup_exercise() alias-aware wrapper

New SQLite table `exercise_aliases (alias, canonical, source)` seeded
with ~40 common gym shorthand entries (OHP, RDL, "Bench", "Squat",
plural/singular drifts, slang). Lookups go through this table first,
then fall through to the strict exercise_db matcher — so the strict
matcher's "false negative for ambiguous single tokens" property is
preserved while still resolving every-day vocabulary.

Schema decision: every seed row is tagged `source='seed'` and re-seeded
on every init_db (deleted-then-reinserted), so editing the seed dict
in code is the one source of truth. User-inserted rows are tagged
`source='user'` and never touched by re-seeding. Migration path covers
existing DBs where the `source` column didn't exist (those rows tagged
'seed' on first migration, then refreshed from the current seed).

New helper db.lookup_exercise(name) wraps the alias resolution + the
exercise_db.lookup() call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Danny 2026-06-01 10:49:24 +03:00
parent ebd0016a62
commit 214596e26f
4 changed files with 233 additions and 26 deletions

View file

@ -44,6 +44,17 @@ class TestLookup:
assert exercise_db.lookup("") is None
assert exercise_db.lookup(" ") is None
def test_short_acronyms_dont_substring_match_characters(self):
# Regression: "RDL" used to match "Hurdle Hops" because "rdl"
# appears as a character substring inside "hu**rdl**ehops".
assert exercise_db.lookup("RDL") is None
assert exercise_db.lookup("OHP") is None
def test_multi_token_requires_full_coverage(self):
# Regression: "BB Row" used to match "Sled Row" because both share
# the "row" token. Strict 100% coverage prevents this.
assert exercise_db.lookup("BB Row") is None
def test_returned_shape(self):
m = exercise_db.lookup("Pullups")
# The slim view drops `instructions` and `images`.