When the gym changes, the plan can change with it
INCREMNT is an iOS strength-training app that tells you what to lift next; this week it made planned workouts easier to adapt to the equipment in front of you while tightening the load, coaching and export contracts behind that decision.
- Built an Adapt workout path for planned days: edit exercises in the typed workout editor, review the diff, then start once or update the program day.
- Added exercise swaps that preserve slot intent—sets, reps, rest, notes, supersets and progression overrides—while keeping the original exercise visible as provenance.
- Tightened load transfer for numbers with different meanings; tests cover a 62.5 kg barbell Romanian deadlift becoming 32 kg per dumbbell for a stiff-legged dumbbell deadlift.
- Built a typed decision path for Ask Coach progress and exercise routes, with a verdict, confidence, evidence references, one checkable next step, repair and conservative fallback paths.
- Prepared Strava strength uploads with native exercise sets and muscle mappings, structured FIT retries and TCX fallback; TestFlight build 407 was prepared, with manual smoke still required.
- Added spreadsheet-friendly CSV export for workout history, and persisted and displayed the post-workout Score contribution when the required data is available.
Product: make the workout fit the room
A planned day can ask for a barbell when the room only has dumbbells. It can ask for a bench when the bench is occupied. The training intent is still useful, but the exact exercise list is no longer practical.
This week I built that decision into the start flow. Choose a planned day, tap Adapt workout, and INCREMNT opens the same typed editor used for writing a workout from scratch. You can select a different movement, change a row, add an exercise, remove one, or change its order. That matters because the real choice is rarely just “which replacement did the algorithm pick?” It’s “what can I do today, and what do I want to keep?”
The review screen makes the answer visible before the workout starts. Swapped exercises show the previous movement. Added, updated and removed rows have their own sections. Prescription edits and reordered rows are called out instead of being folded into a vague “adapted” label. An athlete can open exercise media while reviewing, too, which keeps a replacement from becoming a blind text choice.
There are two deliberate scopes. Just for this workout starts the reviewed day without rewriting the saved program. Update program and start persists the reviewed day as one program change and then starts that persisted version. A hotel session shouldn’t quietly become next week’s new plan, but a change that works well in a different gym shouldn’t have to be rebuilt by hand either.
The boundary is conservative. The saved-program path only applies when the day on screen still exactly matches the day in the store. If another change landed while the sheet was open, the update is rejected and the athlete gets a conflict message instead of losing somebody else’s edit. The one-session path can still use the reviewed day. That is a better failure mode than blocking a useful workout because every original slot lacked a perfect replacement.
I recorded the feature as an iOS-only capability because it changes how a planned day is started, not how the web dashboard represents a program. I also added a separate equipment-and-setup taxonomy to Quick Workout, so “bodyweight” no longer implies “possible in an empty room”: a hanging leg raise still needs a pull-up bar, and a bench press still needs a rack and bench.
Also moved forward
Ask Coach’s bounded progress routes now have a typed decision layer in the implementation. The model must return a constrained mode, verdict, confidence, evidence references, unknowns and—when useful—one reversible next step with a success check. The validator requires the routed evidence, asks for goal calibration when the goal is unknown, checks for an exact load-and-reps comparison in broad reviews, and rejects arithmetic that cannot meet its own rep target. There is one repair attempt; if that still fails, the result becomes a low-confidence “unclear” decision instead of confident prose built on a broken shape. It’s groundwork for advice that can be checked later, not a claim that every Coach path is finished or publicly released.
The Strava path moved from “send a workout file” toward a real strength export. iOS emits a structured set payload with exercise identifiers, reps, weights, timed work and heart-rate streams. The service converts it to a decoder-valid FIT activity with exercise categories and muscle mappings, retries when Strava rejects the structured upload, and falls back to the legacy TCX format for older or less cooperative cases. Stable workout IDs, upload claims and duplicate handling keep a retry from creating a second activity. The tests decode the FIT output and walk the catalog mappings, including the lower-body names covered by the latest mapping tests. Build 407 was accepted as an upload, but the ledger still calls for manual TestFlight smoke, so I’m describing this as prepared rather than shipped.
I built a more useful post-workout Increment Score handoff. When the inputs are available, the summary can show the score before and after the session, the change, component movement and session-relevant drivers. A persistent hidden mode conceals the values, direction and drivers rather than leaking them through a different panel or accessibility label. I also added CSV export with one row per logged set, the athlete’s display units and canonical kilogram columns; the existing JSON export remains the round-trip format. These are small surfaces, but they give the athlete a clearer record and make the data easier to take elsewhere.
Building INCREMNT: a swap is a domain decision, not a string replacement
The hardest bug in this work looked harmless in a test: a barbell Romanian deadlift at 62.5 kg and a stiff-legged dumbbell deadlift at roughly the same number. The exercises share a movement family. The number does not have the same meaning. One barbell value describes total external load; a dumbbell value is recorded per implement.
The old identity model was doing two jobs. It was useful for finding related history, but it also became an accidental permission to reuse the weight directly. That is how a recommendation can look precise while being wrong by a factor of two.
The fix was to model the measurement. ExerciseLoadSemantics carries load and repetition multipliers. ExerciseLoadBasisKey combines canonical identity with the load multiplier. ExerciseLoadEvidence and ExerciseLoadTarget make the source and destination explicit, and ExerciseLoadTransferPolicy converts through total external load before translating reps and rounding to the target exercise’s increment. The swap resolver uses that policy instead of adding another exercise-name heuristic. Session caches, bests, previous sets and recommendation fallbacks now keep load basis alongside identity, so a synonym can share evidence while a different recorded-load meaning cannot.
The concrete regression is now executable: the 62.5 kg barbell observation translates to 32 kg per dumbbell at the same reps. Invalid evidence returns no invented load. A compatible movement can receive translated evidence, while a true alias with the same basis can reuse it directly. Tests also cover role changes—for example, a hypertrophy compound swapped for an isolation movement gets an isolation prescription rather than carrying a compound set-and-rest policy into the new exercise.
The same boundary appears one level up in plan persistence. WorkoutPlanAdaptation is a diff, not a bag of replacements. It records replacements, additions, removals, updates and reordering. Applying it is one store mutation guarded by the exact original day. That gives the UI something honest to review and gives the store a simple stale-write rule.
I accepted more metadata on each exercise, more explicit cache keys and more tests. That’s the right cost. A recommendation that says “32 kg each” is making a decision about what the number means, not merely formatting a scalar.
The idea worth stealing: use identity to find relevant evidence, never as proof that two measurements are interchangeable. Keep the measurement basis explicit, then make every transfer pass through one tested policy.
Tiny wins
- Tightened social likes so mutations serialize across feed and detail surfaces and roll back to the last confirmed state when a request fails.
- Added Apple Watch heart-rate recovery for a stalled stream, with the phone fallback clearing stale values.
- Worked on splitting sync persistence into domain-owned stores, with hosted request-handler ownership covered by contract tests.
- Tightened Mac validation and repository lint rules so the existing Swift debt baseline stays pinned.
What’s next
I’ll keep exercising the adaptation and load-basis boundaries against the long tail of catalog variations, especially where aliases, bodyweight and per-implement loads meet. The next useful proof is not another happy-path screenshot; it’s a reviewable decision that survives the next session.
The Strava mapping work and Coach decision paths still need their remaining live or manual checks before I’ll call them released. In the meantime, the product is getting better at exposing the choice it is making—and at refusing to pretend certainty when the evidence isn’t there.