Skip to content

Per side on timed holds, rebased on v1.3.9 with the progression fix - #322

Merged
DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
horusglez:feat/timed-per-side-v2
Oct 7, 2026
Merged

DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
horusglez:feat/timed-per-side-v2

Conversation

@horusglez

Copy link
Copy Markdown
Contributor

Rebase of the second half of #295 onto v1.3.9, with the progression fix you
asked for. (The photo/GIF half is dropped here — your own v1.3.9 version is
better than what I had, thanks for building it.)

What

Per side (issues #31/#32/#33) was dropped when switching an exercise to
Time, because it counts reps and a timed hold has none to split. "Per side"
doesn't have to mean splitting, though: for a hold it means doing the whole
thing once per side. The flag now survives the switch and buildWorkSets
doubles the planned sets instead of dividing the duration — 2 sets of 30s
becomes 2 left + 2 right, each still 30s. Plain rows tagged side: 'L'|'R',
not the L/R sub-row pair reps mode uses (isSideSet) — a hold has one
duration to log per row, not two independent values to track side by side.
exLine reads 3 × 0:45 · per side.

The progression fix

You were right — readSession (progression.js) reads only the plan's own
sets count off the front of the logged rows (issue #233: a bonus set added
on top shouldn't move the prescription). That's correct for every mode that
doesn't double its row count, but a per-side timed hold does, and planned
was still the pre-doubling number. sets.slice(0, planned) stopped after
the first pair, so the far side's holds never reached ok/held/best —
exactly what you flagged: the right side never counted (in my alternating
L,R,L,R row order, it was actually "everything past the first pair" that
never counted, but same root cause).

Fix: planned doubles too when mode === 'time' && isPerSide(target), so
all 2n rows get read, not just the first n.

Testing

  • npm run build — clean.
  • npm test — 3165 pass on top of v1.3.9's own suite. New: buildSets
    doubling/history-carry for a timed per-side hold, exLine's per side
    wording, and three readSession cases — all-held, a short set buried in
    the second pair (the one the old code never even looked at), and fewer
    logged rows than the doubled count.
  • Same caveat as Custom exercise photo/GIF, and per-side on timed holds #295: I don't have a browser in the environment I built
    this in, so I could not click through the actual screens — logic is
    covered by the tests above, but the UI (the per-side toggle on a Time
    config, the doubled rows with their Left/Right tags during a workout) has
    not been eyeballed by a person yet.

🤖 Generated with Claude Code

Rebase of DuarteSantos8#295's second half onto v1.3.9. Per side (issues DuarteSantos8#31/DuarteSantos8#32/DuarteSantos8#33)
was dropped when switching an exercise to Time, because it counts reps
and a timed hold has none to split. "Per side" does not have to mean
splitting, though: for a hold it means doing the whole thing once per
side, so the flag now survives the switch and buildWorkSets doubles
the planned sets instead of dividing the duration — 2 sets of 30s
becomes 2 left + 2 right, each still 30s. The rows are plain, tagged
with `side: 'L'|'R'`, not the L/R sub-row pair reps mode uses
(isSideSet) — a hold has one duration to log per row, not two
independent values. exLine reads "3 × 0:45 · per side".

Fixes the progression bug @DuarteSantos8 flagged on DuarteSantos8#295: readSession
(progression.js) reads only the plan's own `sets` count off the front
of the logged rows, on the theory that a bonus set added on top should
not move the prescription (issue DuarteSantos8#233) — exactly right for every mode
that does not double its row count. A per-side timed hold does, and
`planned` was still the pre-doubling number, so `sets.slice(0, planned)`
stopped after the first pair and the far side's holds never reached
`ok`/`held`/`best`. Fix: `planned` doubles too when `mode === 'time' &&
isPerSide(target)`, so all 2n rows are read, not just the first n.

npm run build: clean. npm test: 3165 pass, including the new ones —
buildSets doubling/history-carry for a timed per-side hold, exLine's
"per side" wording, and three readSession cases (all-held, a short set
buried in the second pair, fewer logged rows than the doubled count —
the exact scenario the progression bug missed).
@DuarteSantos8
DuarteSantos8 merged commit ff61010 into DuarteSantos8:main Oct 7, 2026
DuarteSantos8 added a commit that referenced this pull request Oct 7, 2026
…al Left/Right pills (#322)

A timed per-side set now pauses ten seconds to switch sides after its first side,
with its own label on the countdown and the chime and buzz of a short timer; the
set's full rest follows once both sides are held. The Left and Right pills take the
width of the longer word, so the inputs after them line up.
@DuarteSantos8

Copy link
Copy Markdown
Owner

Merged and released in v1.3.10, thanks @horusglez!

https://github.com/DuarteSantos8/openGym/releases/tag/v1.3.10

giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 8, 2026
), get_week_plan coach tests

Exercise settings set the exercise's rest on the duration wheel; 0:00 means the profile's rest.
A timed hold can be per side: the planned sets double into a left and a right row, saved with
their side, counted as one set once both are held; the migration, the session-to-routine copy and
the Coach reader follow. The MCP get_week_plan coach-week and pin tests are back on the engine's
fixtures.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 8, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 17 templates
  build it (the 13 v1-era presets plus top_set_backoff, beginner_steps,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 8, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 8, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 10, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 10, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 11, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
@horusglez

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@github-actions

Copy link
Copy Markdown

Thanks for your pull request! Before it can be merged, please read the Contributor License Agreement and sign it by commenting the sentence below. It takes a second and covers all your past and future contributions.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 20, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
giulioleuci added a commit to giulioleuci/openGym-contrib that referenced this pull request Oct 23, 2026
…matic v1 → v2 migration

Replaces the ad-hoc v1 progression code with a rule-based prescription engine, adds a warm-up
planner, and converts every v1 profile automatically and losslessly at launch. Issues DuarteSantos8#156, DuarteSantos8#186.

Engine (api/engine, read by the client through frontend/src/lib/prescription)
- A PlanRule carries one `program`: phases of set groups, an ordered chain of operators (load,
  reps, sets, seconds, rest, rung), exits, a stall recovery and a completion list. 13 templates
  build it (autoregulated, linear, greyskull, double, triple, hold_seconds, bodyweight_ladder,
  pyramid_reps, pyramid in either direction, five_three_one, top_set_backoff,
  accumulation_intensification, density); the engine reads the program, never the preset name.
- Starting a session freezes one content-hashed Prescription per occurrence; a logged exercise is
  an exposure with its performance, actual and audit, so history never changes when a rule is
  edited. The next session's values are decided when a session finishes and replayed identically.
- Target effort (RIR/RPE), intensifiers (drop sets, rest-pause), pyramid sets (pyramid_reps, Max
  sets), timed per-side holds (DuarteSantos8#322), rest on the duration wheel, Update routine, 1RM dictionary.
- progression.js and finish-workout.js are gone; finish-session.js reads v2 work sets only.

v1 behaviour, followed exactly (checked against v1's own code, git ce30c73)
- The session's weight is v1's readSession.weight (heaviest prescribed set, least help on an
  assistance machine): sessions are held at it, step and back off from it; a weight not typed
  asks for the plan's; an exercise without progression opens at the routine's weight; a timed
  hold at what was lifted last time.
- Double progression opens at its top, counts an unchecked set as 0 reps, never aims below the
  range, ends a run of misses when a session beats the best one at that weight.
- Bodyweight climbs go one rep over the target asked for; no repsMax is no ceiling; loaded work
  on a bodyweight exercise opens at the plan's reps and backs off without Epley (v1 isBw).
- Stall runs are kept by the weight lifted, so a timed hold backs off again after a further miss;
  increments and back-offs land on the increment's grid and keep an off-grid weight off-grid
  (addStep, DuarteSantos8#175); rest-pause is judged against the plan's reps, a per-side set by its total;
  "start from your last session" reopens each row at what it did.
- Not copied: rest-pause planned for more than one set, which v1 never progressed by accident.

Warm-ups (DuarteSantos8#156)
- Off, smart ramp or percentage template per exercise; ramp rows are frozen in the prescription
  and only untouched automatic rows are re-aimed when the work weight changes mid-session.

Migration (api/migration, shared by the API, the browser and the Capacitor shells)
- Deterministic v1 → v2 conversion: each logged session is frozen as the routine's own rule at
  the point v1's target named (generatePrescription `at`), and the track state is replayed from
  them, so the first session after the upgrade opens where v1's would have.
- A workout still running at the upgrade keeps v1's rows and finishes into the next session.
- At launch the app finds any v1 copy (server, browser, phone mirror), backs each one up, converts
  it and asks the server to convert its own (which keeps state-<uid>.pre-engine-v1.json), with
  nobody asked; a v1 backup file being imported still asks OK or Cancel.
- Lossless compact storage and wire form (profile-pack.js) keeps heavy users under the 16 MiB
  sync cap; malformed v1 data is repaired or recorded in migrationAudit, never fatal.

Consumers: Coach, stats, admin, plan share, printable plan, MCP server and units conversion read
v2. Docs: docs/MIGRATION_TO_ENGINE_NOTE.md (both data models), REPORT.md (migration audit).

Tests: API 726, MCP 50, frontend 4604, including a seeded migration fuzz suite and
migration-sessions.test.js, which replays v1-recorded multi-session histories (migrated at the
first session, after three, and mid-workout) and checks every session against v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NjHLLEQrs4ux7bRYPguAn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants