From aabc3df4358814519a0d059e8a22426086df4eb0 Mon Sep 17 00:00:00 2001 From: wromansky <25023897+wromansky@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:03:48 -0400 Subject: [PATCH] feat(routines): return the day's description from get_workout_for_date A routine's per-day description is where the notes live that the numbers alone do not carry: rep ranges, which machine substitutes for which, form cues. get_workout_for_date is documented as "the one call that answers 'what am I doing today'", and it was returning the prescribed sets, reps and RiR while dropping the field that says on what terms. The gap is not academic. A day whose description reads "working reps = lower end of range: bench 6-8, press 8-10" prescribes a range, and a caller working only from the payload reports "6" and "8" as fixed targets. The information exists, one field away, on an object this function already resolves. Unset comes back as null, the same treatment day_name already gets, so the field is always present and never leaks a sentinel across the tool boundary. --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- src/wger_mcp/tools/routines.py | 12 ++++++++++++ tests/test_routine_logging.py | 14 +++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c8720..e064e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ notes. This file records important changes to *this package*. ## Unreleased +* `get_workout_for_date` returns the day's `description` as `day_description`. + A routine's per-day notes are where rep ranges, machine substitutions and + form cues live, and the tool that answers "what am I doing today" was + returning the planned numbers without the terms they were written under — a + caller reporting the plan quoted a bare rep count where the routine had + specified a range. Unset descriptions come back as `null`, matching + `day_name`. + ## 0.2.0 * `add_exercise_with_sets` returns the created ids, as its docstring always diff --git a/README.md b/README.md index c21dad1..5448027 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ The training-plan tree is the largest group and splits in two: `routines_read` ( | `set_slot_entry_config(slot_entry_id, kind, value, iteration?, operation?, step?, repeat?, weight_unit?, requirements?)` | Add a per-iteration config record. `weight_unit` applies to `kind='weight'`/`'max_weight'` and is recorded on the slot entry. `requirements` gates the step on what was logged — any of `repetitions`, `weight`, `rir`, `rest` | | `update_slot_entry_config(kind, config_id, value?, iteration?, ..., requirements?)` / `delete_slot_entry_config(kind, config_id)` | Patch / delete a config record (use to bump weight on progression). `requirements=[]` clears an existing gate | | `add_exercise_with_sets(day_id, exercise_id, sets, reps, weight?, slot_order?, weight_unit?, rir?, entry_type?)` | Convenience: slot + entry + sets/reps configs in one call. Omit `weight` to prescribe sets without a load | -| `get_workout_for_date(routine_id, workout_date?)` | What the routine prescribes on a date (default today): one entry per planned SET, with exercise name, `slot_entry_id`, reps, weight and RiR. Feed its ids into `log_set` | +| `get_workout_for_date(routine_id, workout_date?)` | What the routine prescribes on a date (default today): one entry per planned SET, with exercise name, `slot_entry_id`, reps, weight and RiR, plus the day's own `day_description` notes. Feed its ids into `log_set` | ### Workout logs diff --git a/src/wger_mcp/tools/routines.py b/src/wger_mcp/tools/routines.py index 23cbbf9..ff901e4 100644 --- a/src/wger_mcp/tools/routines.py +++ b/src/wger_mcp/tools/routines.py @@ -363,6 +363,12 @@ async def get_workout_for_date( repetitions, weight and RiR. Feed routine_id, slot_entry_id and iteration straight into log_set so the logged set attaches to the plan. + day_description carries the routine's own notes for that day — rep + ranges, machine substitutions, form cues — as the trainee wrote them. + The planned numbers say what to do; the description says on what terms, + and a caller that reports the plan without it quotes a bare rep count + where the routine specified a range. + This is the one call that answers "what am I doing today" and "what is in this program". Walking days, slots, entries and their configs costs dozens of requests and returns far more than anyone needs. @@ -406,6 +412,12 @@ async def get_workout_for_date( # A day need not be named, and Unset would not survive the # tool boundary as JSON. "day_name": None if isinstance(day.name, Unset) else day.name, + # Where a routine keeps its per-day coaching notes: rep ranges, + # machine substitutions, form cues. Without it a caller has the + # numbers but not the terms they were written under. + "day_description": ( + None if isinstance(day.description, Unset) else day.description + ), "is_rest_day": (day.is_rest is True) or not planned, "planned": planned, } diff --git a/tests/test_routine_logging.py b/tests/test_routine_logging.py index f25b98a..da51872 100644 --- a/tests/test_routine_logging.py +++ b/tests/test_routine_logging.py @@ -122,7 +122,13 @@ def _sequence( "iteration": 3, "date": day_date.isoformat(), "label": "Week 3", - "day": {"id": 11, "routine": 7, "name": "Push", "is_rest": is_rest}, + "day": { + "id": 11, + "routine": 7, + "name": "Push", + "is_rest": is_rest, + "description": "Working reps = lower end of range: bench 6-8.", + }, "slots": slots, } ) @@ -172,6 +178,9 @@ async def test_returns_slot_entry_ids_for_today(monkeypatch: pytest.MonkeyPatch) assert out["iteration"] == 3 assert out["day_name"] == "Push" + # The day's notes carry the terms the numbers were written under - a rep + # range here - so a caller reporting the plan can quote them. + assert out["day_description"] == "Working reps = lower end of range: bench 6-8." assert out["is_rest_day"] is False assert len(out["planned"]) == 1 entry = out["planned"][0] @@ -222,11 +231,14 @@ async def test_unnamed_day_still_answers(monkeypatch: pytest.MonkeyPatch) -> Non mcp = _register(routines) sequence = _sequence() sequence[0].day.name = UNSET + sequence[0].day.description = UNSET monkeypatch.setattr(routines.routine_date_sequence_gym_list, "asyncio", _Capture(sequence)) _mock_names(monkeypatch) out = _result(await mcp.call_tool("get_workout_for_date", {"routine_id": "7"})) assert out["day_name"] is None + # Same treatment as the name: Unset must not survive the tool boundary. + assert out["day_description"] is None assert len(out["planned"]) == 1