Repository navigation
Per side on timed holds, rebased on v1.3.9 with the progression fix - #322
Merged
DuarteSantos8 merged 1 commit intoOct 7, 2026
Merged
Conversation
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
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.
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
Contributor
Author
|
I have read the CLA Document and I hereby sign the CLA |
|
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
buildWorkSetsdoubles 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 oneduration to log per row, not two independent values to track side by side.
exLinereads3 × 0:45 · per side.The progression fix
You were right —
readSession(progression.js) reads only the plan's ownsetscount off the front of the logged rows (issue #233: a bonus set addedon 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
plannedwas still the pre-doubling number.
sets.slice(0, planned)stopped afterthe 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:
planneddoubles too whenmode === 'time' && isPerSide(target), soall
2nrows get read, not just the firstn.Testing
npm run build— clean.npm test— 3165 pass on top of v1.3.9's own suite. New:buildSetsdoubling/history-carry for a timed per-side hold,
exLine'sper sidewording, and three
readSessioncases — all-held, a short set buried inthe second pair (the one the old code never even looked at), and fewer
logged rows than the doubled count.
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