Dev Diary

When intent survives the swap

INCREMNT is an iOS strength-training app that helps athletes follow a plan and decide what to lift next; this week’s work made that intent hold together when exercises are swapped, sets are interpreted, and a workout moves between phone and Watch.

  • Exercise swaps now open into a recommended-or-browse-all flow with filters for focus, movement type, muscle, equipment, and completed history.
  • Permanent program swaps keep their relationship in change history instead of leaving transient swap markers on every future workout.
  • Progression now aligns completed sets to the frozen prescription before carrying targets forward or explaining the next recommendation.
  • iPhone and Apple Watch exercise navigation now reconcile local choices without letting an older device update win by accident.

Product: the movement can change without losing the plan

INCREMNT is built around a simple promise: a plan should help you make the next training decision. The promise gets tested when the room, the equipment, or the athlete’s preference changes.

This week I made exercise replacement easier to use without making it a blind choice. When an athlete swaps a movement, the sheet now starts with recommended replacements and offers a separate Browse all surface. Search still works, but the broader catalogue can now be narrowed by upper or lower focus, push or pull, compound or isolation, muscle group, equipment, and whether an exercise has already been completed in the athlete’s history. A completed-history filter is based on canonical exercise identity, so a renamed or synonymous movement doesn’t disappear from the record.

That sounds like a catalogue feature. It’s really a decision boundary. “Give me another chest movement” and “show me a cable isolation movement I’ve already used” are different requests, and the UI should let the athlete make that distinction. The taxonomy keeps those filters explicit instead of hiding them inside another ranking heuristic. It also handles the awkward cases: deadlifts belong with lower-body browsing even when their catalogue muscle is back, and delt movements can land in push or pull based on the actual exercise rather than a single broad label.

The swap itself remains conservative. A permanent program replacement is the new exercise in the saved slot; the old/new relationship lives in change history. That means future workouts don’t keep announcing a swap that already happened, while the history still explains how the plan changed. A one-workout substitution can keep its provenance for the session without polluting the saved program.

The practical benefit is that an athlete can adapt to the room and still recognise the workout they meant to do. The movement changes. The prescription, rest, notes, and progression choice don’t silently change meaning with it.

Building INCREMNT: one frozen prescription, many consumers

The difficult part was not drawing more filters. It was keeping every downstream consumer aligned to the same workout snapshot.

A completed session can contain warm-up rows, working rows, extra rows, changed loads, and data written by older versions of the app. Recommendation generation already knew how to interpret the frozen prescription attached to that session. Program carry-forward and the post-workout explanation were using slightly different shapes. In the legacy case, ramp sets had been stored without an explicit warm-up flag. One path excluded the ramp prefix; another treated the same rows as working sets. The recommendation could be reasonable while the next saved target or its explanation was based on a different set count.

I pulled the alignment into a shared helper. It first looks at the frozen target sets. If an old snapshot has no warm-up markers, it infers a ramp prefix only when the completed rows match that prefix from the first row. A later matching weight is ambiguous: the athlete may have deliberately used it for a working set, so it stays as evidence. The helper then feeds recommendation generation, program carry-forward, and completion copy. The rule is narrow because a false warm-up inference is worse than retaining an extra row for review.

The same principle now travels through a swap. When a program slot is replaced, the new exercise can preserve the original prescription where that is the explicit choice, but the saved object no longer carries transient swappedFrom or substitution-origin flags. The durable relationship is represented by applied change history. On load, a repair pass clears only the old markers proven by that history; genuine workout-only provenance is left alone. This is a useful distinction between state that describes the current plan and state that describes how one session got there.

The recommendation explanation also stopped estimating a rep increase from set count when the exact baseline and target arrays are available. It now calculates the total change from those arrays, then falls back to the older per-set calculation only when the shapes do not match. The sentence shown to an athlete is therefore derived from the same target that the system will apply, rather than from a convenient approximation.

Cross-device navigation exposed a different version of the same problem. The Watch can receive a phone update while an athlete is swiping locally. Treating every timestamp as a global ordering lets a legitimate phone save reject a Watch cursor change, or lets an older device jump the Watch backwards. I separated Watch cursor ordering from ordinary draft saves, keyed the ordering to the workout start, and keep a pending local selection until the phone confirms the same page. If the workout is already complete, the local selection is discarded and the completed screen wins.

These boundaries are covered in focused tests: legacy unflagged ramps, explicit set roles, extra working sets, permanent swap provenance, canonical completed history, and Watch reconciliation. The tests are not just checks on output strings. They pin the order in which state is interpreted and applied.

There’s a recurring cost here: more metadata, more pure logic, and more fixtures for old data. I’m taking that cost. A training recommendation is a claim about intent. If two parts of the app disagree about which rows or which exercise a claim refers to, the number may still look polished, but it isn’t dependable.

Also moved forward

  • The weekly Dev Diary workflow now builds the exact evidence window, validates one thesis-led article, recalculates reading time, and publishes only after the generated website change passes its checks. The workflow keeps a pull request as an audit trail and refuses duplicate week entries.
  • PostHog AI-generation events gained bounded lifecycle coverage, while the hosted MCP work moved closer to ChatGPT launch review. Both remain implementation and preparation work rather than claims of public release.

What’s next

I’m going to keep testing the long tail where exercise identity, load meaning, and old session formats overlap. The next useful proof is a swap that remains understandable after the athlete completes it on one device and resumes it on another.

This diary was generated and edited from the week’s commits, pull requests, tests, and development notes using AI, evidence-checked, and automatically published, and may describe work not yet publicly released.