Arbitrary but consistent
A gym's assessment answers become rules that swap an exercise for a member before a session. Two rules can disagree about the same movement. The code says who wins, and the comment admits how.
4 min readMicroGym · Assessment stage
The live-operations tool runs a small-group session on a tablet: one workout template, eight people in the room, and a grid where the trainer records every set. The people are not the same, so the template cannot be the plan for any one of them. The piece that turns one template into eight plans is an assessment and substitution engine, 1,507 lines in one file, and this note is about the decisions inside it, including the ones it gets only mostly right.
A living profile, not a form
An assessment here is not an event with a date and a score. A gym defines a flat list of questions, and every member carries a current answer to each one. The comment at the top of that service states two invariants and says that breaking either makes substitutions silently stop working: a member's answers must sit on the same template as the questions they answer, and an option's value is an immutable join key, never a display position. That second one is the kind of rule that is obvious once written and expensive to discover by moving an option in a list.
The rule
A substitution rule is four things: a movement, a question, an answer value, and the movement to use instead. That is all. There is no severity, no body area, and only one action, swap. Reducing the load, skipping the set, capping the reps, none of that exists as a rule. An older path keyed on a fixed list of injury types sits beside it. The narrowness is a choice: a rule a trainer can read in one line is a rule a trainer will write.
Who wins
The rules run once, when the session is created. The engine collects each member's most recent answer per question, finds every rule that matches, and then faces the conflict the title is about: two answers can point the same movement at two different substitutes. The resolution is newest assessment wins. When the dates are equal, the branch is empty except for a comment: keep the existing one, arbitrary but consistent.
I would rather have that comment than a rule pretending to be principled, and I would also rather tell you the part it does not say. The query that fetches the rules has no ordering clause, so which rule the engine sees first is whatever order the database returns, and the consistency the comment promises rests on that order not changing. Nothing enforces it. Writing that down here is cheaper than the afternoon someone will otherwise spend on it.
What the engine may not touch
The engine writes one thing: a default substitution row for a member and a movement, and only if none exists. It never writes to the template. That separation is structural rather than a check, because no session or substitution code path writes to the template's rows at all, which is a stronger guarantee than a guard someone can remove. A trainer can override any default for one session, and the session-scoped row wins over the engine's row when the grid renders. If the engine fails while a session is being created, the failure is logged and the session is created anyway, because a session without automatic substitutions is still a session, and no session at all is a room full of people waiting.
The number in the cell
Each cell on the tablet needs a suggested weight before the trainer types one. With history, the engine estimates a one-rep max from the member's last set with the Epley formula, inverts it for the target reps, and rounds to the nearest five. Without history, the movement carries default targets for three strength levels and the member's intake level picks one. When a substitution is active, the defaults come from the substitute movement, not the original, which is the detail that stops a member with a knee limitation being handed a squat's numbers for a hinge. Epley is known to drift at high repetitions and the code does not say so where it is used. It should. This site said it did, until the source was re-read for this note.
What it cost
The file has fourteen exports and three of them have a caller outside it. Two comments left behind by an assisted edit, of the kind that says keep all the rest of the existing functions exactly as they are, are still in the live file. The generated database types name a table or two the code no longer reads, and the code reads a few the types no longer name, so the types were last regenerated in July 2026 and the ledger of thirty-four tables carries that caveat. There are no tests over the engine, the tie-break, or the arithmetic, and no continuous integration, said plainly on the platform's own card. The gym runs its sessions on it every week regardless, which is the argument for fixing the list above in the order it is written.
The pattern the site keeps returning to is here again, on the other side of the money and the security policies: the person who wrote the assessment questions, the rule table, the grid and the arithmetic is the same person, so a wrong number in a cell is one repository away from its cause. The corrections on this page are proof of the same thing from the other direction. The anatomy rows for this platform were written partly from memory, the source was re-read on 4 September 2026, and four of them were changed to what the code shows.