Docs
/

@leumas/gym

Gym engine — a clean-room implementation of self-hosted strength training: four progression policies (linear, Greyskull LP, double, timed) with stall detection and deloads, estimated 1RM, RIR/RPE...

@leumas/gym

The gym engine: everything a self-hosted gym needs that is not storage, transport or pixels.

Zero dependencies, pure isomorphic ESM. The Studio session runner imports it in the browser; /api/gym, the gym app's actions and MCP import it on the server. There is exactly one copy of this arithmetic, which is the point — a training rule that lives in two places is a rule that will disagree with itself the first time one copy is fixed.

SPEC-ONLY provenance: no openGym source was copied. See ATTRIBUTION.md, which also carries the licence boundary around exercise media. Read it before adding data.

What it absorbs

AreaEntry pointWhat it answers
Progression@leumas/gym/progressionGiven everything logged, what should the next session prescribe — and why?
Estimated 1RMestimate1RM, e1rmSeries, is1RMRecordHow strong is this lift, and did today beat it?
EfforteffortSummary, effortHistogramHow hard was the training, and how much of it was even rated?
VolumemuscleVolume, weeklyVolume, sessionTotalsHow much work, and which muscles got it?
HistoryactivityHeatmap, streaks, personalRecords, bodyweightSeriesWhat has happened over a year?
Catalog@leumas/gym/catalog1,324 exercises: normalise, search, resolve a free-text name.
Muscles@leumas/gym/musclesWhich muscles an exercise trains, and how to shade a body map.
Import@leumas/gym/importFitNotes, Strong, Hevy, Apple Health — as a proposal, never a write.
PlanningbuildStarter, weekOf, reschedule, expandSessionStarter programs, the week, and turning a plan day into sets.

The four progression policies

All four share one signature and one judgement.

import { next } from '@leumas/gym/progression';

const prescription = next({ history, config: routineItem, routine, unit: 'kg' });
// { policy, kind, weight, reps, sets, seconds, deloaded, reason }
  • linear — every rep of every set and the weight goes up; three misses deload 10%.
  • greyskull — two straight sets and a last set to failure; double the target there and the jump

doubles; one failure resets 10%.

  • double — climb a rep range at one weight, then add load and drop back to the floor.
  • timed — no load to add, so the duration climbs; deloads in whole 5s steps.

kind is off | first | up | hold | deload. A field the policy has no opinion on comes back undefined — not 0 — so the caller keeps what the plan said. reason is { code, text, values }: a stable i18n key plus an already-interpolated English sentence, because a suggestion you cannot audit is one you stop trusting.

Three things that are easy to get wrong

Reading a session honestly is the whole game. A completed set with fewer reps than its target is a miss. A set never ticked off is a miss. Fewer sets than prescribed is a miss. Soften any of those and a session that fell apart advances the load as though it had succeeded. See src/progression/judge.js.

A set carries the target it was prescribed. expandSession stamps targetWeight/targetReps/ targetSeconds/targetSets onto every generated row. A set with no stamped target cannot be scored against anything, and history full of them makes every past session read as a miss — which greets a long-standing member with "missed reps eleven sessions running, deload".

Bodyweight is decided by the LOGGED weight, not by a flag. A dip done with a belt has a load to progress and belongs on the normal policies; a barbell lift logged at 0 has nothing to add to.

Day boundaries

Every function that buckets by day takes utcOffsetMinutes and defaults it to 0 (UTC), not the server offset. Passing the member's offset is what makes "today" mean their today; a session logged at 22:30 local is the next day in UTC, which draws the heatmap one square off and breaks a streak nobody broke. The default is UTC rather than the server's because a wrong answer that moves when you redeploy is worse than one that is wrong the same way twice.

Regenerating the catalog

git clone --depth 1 --filter=blob:none --sparse https://github.com/hasaneyldrm/exercises-dataset
git -C exercises-dataset sparse-checkout set data      # data/ ONLY — never images/ or videos/
node ops/tools/gym-catalog/index.mjs --source exercises-dataset/data/exercises.json
node ops/tools/gym-catalog/index.mjs --source exercises-dataset/data/exercises.json --check

Output is deterministic, so a dataset refresh is a reviewable diff rather than a blob nobody reads.

Tests

pnpm --filter @leumas/gym test

137 tests, including every starter program resolving against the real 1,324-exercise catalog and every muscle spelling in the shipped data resolving through ALIAS — so a dataset refresh that introduces a new word fails the build instead of quietly dropping an exercise off the body map.

What it exports

132 exports: 37 components, 0 hooks, 95 helpers. Import from @leumas/gym.

ALIASALIASBARSBUCKETSBY_BODY_PARTBY_BODY_PARTDAYSDEFAULT_FORMULADEFAULT_SECONDS_INCREMENTDEFAULT_SECONDS_INCREMENTDELOAD_AFTERDELOAD_AFTERDELOAD_FACTORDELOAD_FACTORFORMULASHARD_RIRHEAVY_BODY_PARTSHEAVY_BODY_PARTSINERTINERTKG_PLATESLB_PLATESLB_TO_KGLB_TO_KGMAX_BODYWEIGHT_SETSMAX_BODYWEIGHT_SETSMIN_RATEDMUSCLE_LABELMUSCLE_LABELMUSCLESMUSCLESREP_CAPSECONDARY_WEIGHTSECONDARY_WEIGHTSECONDS_STEPSECONDS_STEPSTARTER_PROGRAMSactivityHeatmapapplyPrescriptionapplyPrescriptionatRiskbest1RMbestSetOfbodyweightSeriesbuildStarterdayKeydefaultIncrementdefaultIncrementdeloadTodeloadTodescribeLoadingdetectSourcedetectSourcedisplayScalee1rmSerieseffortHistogrameffortSummaryestimate1RMexpandSessionfacetsOffacetsOffillfilterExercisesfilterExercisesgoalProgressgoalsWithProgresshasEffortimportAppleHealthBodyweightimportAppleHealthBodyweightimportWorkoutsimportWorkoutsinstructionsForinstructionsForis1RMRecordisBodyweightisBodyweightisHardSetlevelsOflevelsOfloadBarloadOfloadOfmapHeadermapHeadermediaUrlmediaUrlmissedThisWeekmodeOfmodeOfmusclesOfmusclesOfmuscleVolumenearestLoadablenextnextnormalizeCatalognormalizeCatalognormalizeExercisenormalizeExerciseparseCsvparseCsvpersonalRecordsplannedMuscleVolumepolicyForpolicyForrankOfrankOfreadSessionreadSessionrepSteprepSteprescheduleresolveExerciseresolveExerciserestAfterrirOfsessionsForsessionsForsessionTotalssnapsnapstallCountstallCountstepEffortstreakstargetOftargetOftoKgtoKgtoScaleweeklyVolumeweekOf
Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt