Skip to content

feat(engine): generic prescription engine v2, smart warm-ups and v1→v2 migration - #312

Draft
giulioleuci wants to merge 12 commits into
DuarteSantos8:mainfrom
giulioleuci:feat/generic-engine-v1.3.12-issue186
Draft

giulioleuci wants to merge 12 commits into
DuarteSantos8:mainfrom
giulioleuci:feat/generic-engine-v1.3.12-issue186

Conversation

@giulioleuci

@giulioleuci giulioleuci commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replaces the ad-hoc progression code with a generic, rule-based prescription engine (v2), adds an intelligent warm-up planner, and ships an automatic, lossless v1 → v2 data migration. Delivered as one squashed engine commit plus six follow-up commits (migration audit and hardening, below).

Related issues: #156 (automatic warm-up generation), #186 (generic v2 training engine).
Milestones: v1.3.12 — Progression engine I and v1.3.13 — Progression engine II.

What changed

Prescription engine (frontend/src/lib/prescription/, api/engine/)

  • 12 progression presets on one shared rule model: manual, autoregulated, linear, Greyskull LP, double, triple, duration/timed holds, bodyweight ladder, pyramid, reverse pyramid, and more.
  • Target effort (RIR/RPE) with a floor: the load is held when the weakest set beat the RIR target.
  • Per-set reps on pyramid offsets plus an RPT builder; hold_seconds preset for bodyweight holds that climb in seconds.
  • Intensifiers (drop-set, rest-pause) gated per rule and wired into live sessions.
  • Bodyweight scale, plate-step and bar-weight aware load rounding.
  • progression.js and finish-workout.js are removed; finish-session.js reads v2 work sets only (PRs and best weights ignore warm-ups).
05-double-target 03-preset-picker 02-config-top

Intelligent warm-ups (#156)

  • Pure planner + validator with three modes per exercise: Off, Smart ramp, Percentage template.
  • Smart ramp scales the number of sets with load and exercise fatigue; the estimate is shown live in the editor.
  • Warm-up rows are snapshotted in the prescription, and only untouched automatic warm-ups are re-aimed when the work weight changes mid-session.
  • Legacy warm-up counts migrate on load, restore, sync and plan import.
07-advanced-options 08-warmup-smart 09-warmup-template

Data migration

  • Deterministic v1 → v2 profile transformer shared by browser, Capacitor, backup import and server.
  • Per-profile server migration endpoints with an immutable v1 backup; blocking upgrade screen (MigrationGate).

Consumers moved to v2

  • Coach (payload, fingerprint, cohort, accepted changes), coach demo, stats (effort, muscle balance, admin), printable plan and equipment check all read v2 plans and sessions.

Migration audit and hardening (follow-up commits)

An independent audit of the v1 → v2 migration against v1's own nextPrescription (findings and reproductions in REPORT.md), then a seeded fuzz and edge-case suite. Result: no v1 document can leave the migration stuck.

  • Progression parity with v1 (loaded lifts on grid-aligned weights match v1 exactly in 1,200 differential trials): a skipped exercise is no longer a miss; the rounding step keeps v1's loads (1.25 kg plates, off-grid machines, held loads); ladder, double and "lifted load" semantics restored; a loaded lift that never had a weight is held like v1 instead of opening at one increment.
  • Size: the v2 profile is stored and sent in a lossless compact form (api/migration/profile-pack.js), about half the size, so heavy users stay under the 16 MB sync cap.
  • Never fatal: unparseable dates, absurd sets, bad units, fractional/negative/overflowing logged numbers, junk rest-pause clusters, non-scalar ids, malformed Coach snapshots and stray v2 keys on a v1 document are repaired or recorded in migrationAudit; a missing profile-validation.js is now tracked.
  • Tests: api/test/migration-robustness.test.js checks, for any generated or corrupted v1 profile: no throw, valid canonical output, input untouched, deterministic, JSON and wire round trip lossless, and a generable first session. Each bug it found has a regression test that fails on the old code. FUZZ_N=8000 FUZZ_HARD=60 node --test api/test/migration-robustness.test.js widens the search.
  • Remaining, documented differences from v1 (double progression after an unchecked set, rep climb after an over-performed session, timed hold after a deload run) are listed in REPORT.md §9.

Tests: API 633, frontend 3305, MCP 29 — all passing.

Docs

  • README, docs/ and mcp/ aligned with the v2 engine and smart warm-ups.

🤖 Generated with Claude Code

https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw

@giulioleuci

Copy link
Copy Markdown
Contributor Author

Implement #156 and #186

@DuarteSantos8

Copy link
Copy Markdown
Owner

Thanks @giulioleuci, that's a big piece of work. It's on the progression-engine milestones (v1.3.12 and v1.3.13), and I'll review it there rather than squeeze it in now.

v1.3.9 changed the code this replaces: the plan owns a session's sets and reps (#275), progression reads per routine slot (#216), an edited plan restarts progression, and double progression only goes up at the top of the range (#297). The v1 → v2 migration will need to carry planned, rid and the "Planned sessions start from" setting. Could you rebase onto v1.3.9 when you get to it?

Also: does this replace #168 and #181 (and #179/#186 on the issue side)? If so, I'll close those in favour of this one.

@giulioleuci

Copy link
Copy Markdown
Contributor Author

Hi @DuarteSantos8!

  • Yes, it replaces all the issues and PRs you listed.
  • Yes, I will be able to rebase after 1.3.9.

@giulioleuci
giulioleuci force-pushed the feat/generic-engine-v1.3.12-issue186 branch 2 times, most recently from 48722e9 to b81d3de Compare September 29, 2026 14:03
@giulioleuci
giulioleuci force-pushed the feat/generic-engine-v1.3.12-issue186 branch 4 times, most recently from 7d709f8 to b793105 Compare October 4, 2026 11:06
giulioleuci and others added 12 commits October 7, 2026 21:52
…tic v1 → v2 migration

Replaces the v1 progression code, which re-derived the next load from history on every read,
with a generic training engine that stores what it decided. Existing profiles are converted
once, on the first launch of the updated app.

Pure, catalogue-free modules that run unchanged under bare node (API) and Vite (web,
Capacitor). The same inputs always give the same prescription.

- Plan rules: a `PlanRule` per routine slot, chosen from 12 presets (manual, autoregulated,
  linear, greyskull, double, triple, duration, hold_seconds, bodyweight_ladder, pyramid,
  reverse_pyramid, five_three_one), with validation and increment/gate vocabularies.
- Prescriptions: generated from the rule, the stored progression state and the current 1RM, then
  frozen and content-hashed. A logged exposure points at the exact prescription it was asked to
  perform, so history no longer changes when a rule is edited.
- Progression: `ProgressionState` per track, advanced from the audited performance of each
  exposure and rebuilt by replaying history when the plan changes.
- Deload: `rule.deload { after, factor }`, stalls counted from the stored state, v1's Epley
  selection for linear/double and the plain factor for the rest; a timed hold slides its
  window back.
- 1RM: append-only `oneRepMaxes` dictionary with selectable formulas.
- Warm-up planning, cardio parameters (minutes and speed), timed holds, per-side rows,
  rest-pause bursts, drop sets, and assistance machines that run every step the other way
  (less help is progress).

`migrateProfileV1ToV2(state, catalogue)` is a pure, deterministic function shared by the API,
the browser and the Capacitor shells, so a retry after a crash, or a device converting its own
copy, yields the identical document. The profile is marked `engineSchemaVersion: 2`.

- The untouched v1 bytes go to an immutable backup before anything is converted; the conversion
  never edits them.
- A blocking screen (MigrationGate) asks the owner to confirm; the server gates every data route
  with `engineGate` until `POST /api/data/migrate-engine-v2` has run. Local copies, guests, the
  phone and v1 backup files are converted through the same function.
- Every v1 field was audited against the converted profile: an exercise with no rule of its own
  inherits its routine's (else linear on reps); double-progression windows keep both bounds;
  the bodyweight ceiling bounds the ladder; `warmupRestSec` and v1's body-part default load step
  are kept; what v1 prescribed for an entry no prescription can hold stays on the exposure
  (`legacyTarget` / `legacyPlanned`).
- History is replayed so progression state, 1RMs and runs of misses carry over.
- The workout in progress moves to its own unsynced key (`gym_active_v1`).

- `lib/progression.js` and `lib/finish-workout.js` are removed; `lib/prescription/` is the
  client's view of the engine and `lib/finish-session.js` / `lib/session-ui-adapter.js`
  reduce a completed session back into state. A test pins that the legacy policy vocabulary
  only survives at the compatibility boundary.
- Rule editor ("Back off when stuck", "Reduce assistance by"), workout screen notes when a
  load was backed off, cardio rows in minutes and speed.
- Session history editor for late-logged days and history edits; cached 1RM and
  muscle-recovery calculations.
- All 16 locales carry the new strings.

- API routes, OpenAPI spec and the Coach payload/plan view read the v2 profile.
- The MCP tools read exposures and their frozen prescriptions, and refuse a profile still on
  the v1 shape with an explicit "upgrade from the app first" message instead of reporting it
  empty.

- docs/MIGRATION_TO_ENGINE_NOTE.md describes the v1 and v2 data models field by field, how
  history and the workout in progress are converted, and the cases that need care (A1–A22).
  SELF_HOSTING.md links it.
- CLAUDE.md and AGENTS.md describe the v2 layout: the engine and migration folders, the
  stored-prescription data model, the engine gate and the new test and Docker/CI touchpoints.
- PROJECT_CONTEXT.md (drop-set / rest-pause notes) is brought up to date with where intensifiers
  live in v2; CONTRIBUTING.md points at the engine and says that new progression presets can be
  created, with the files a new preset touches.

- Migration integrity (api/test/profile-migration.test.js), engine gate, per-module engine
  suites, prescription lifecycle, and the session workflows.
- Two CI fixes from main are kept: the unwritable ./data checks are skipped when running as
  root (GitLab CI container), and the prefetch check passes a missing navigator as null, since
  Node 22 has a global one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1894bxX4Dc9t7x7JjVziY
… caps (M1-M4, M6-M8, M10, M13)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw
…oad, proven on migrated history (P2-P4)

Tests updated because they encoded the old behaviour: advance.test.js (a lighter load is no miss),
assisted.test.js and profile-migration.test.js (the help given does not decide a hit),
generate.test.js (a double opens at last result + 1; extra sets do not move the plan),
engine-attention.test.js A36 (low + step after a miss).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw
…ve the migration stuck

api/test/migration-robustness.test.js drives generated and corrupted v1 profiles through the
migration (never throws, valid canonical output, input untouched, deterministic, JSON/wire
lossless, first session generable) plus named edge cases. Bugs it found, each with a regression
test that is red on the old code:

- fractional logged reps and unloaded plans of > 6 sets made the first session throw (engine)
- negative / overflowing logged numbers, junk rest-pause clusters and totals, array/object ids,
  Coach snapshots with non-list routines, and stray prescriptions/progression/oneRepMaxes or
  packed/templates keys on a v1 document produced an invalid profile
- sets above 50 are clamped with an audit entry instead of silently
- a loaded lift that never had a weight is held like v1 (no increment, stall, deload or rep
  climb) instead of opening at one increment

profile-validation.js was imported by committed code but untracked; it is added here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw
MAX_SYNC_BODY is 16 MiB (api/migration/profile-size.js), but the API reference and the
nginx template still said 5 MiB — a whole workout history grows past the documented cap
long before the real one, and nginx would have answered 413 first.
The running workout lives in A (not S.active) everywhere the 1.3.10 code reads it: the editor,
reminders, rotation restart, holds restored after a reload, tab joins, stashes (converted to the
profile's unit), backups and the in-workout settings. Repeat today builds its session from the
engine (session-repeat.js), a session left open ends at its last set again (session-end.js), the
data routes answer 503 for a state file that does not parse, and a broken /api/push/public-key
block in openapi.yaml is repaired (website/api.html regenerated). Locales: the stale em-dash
strings go, zh-TW gets the engine strings. Tests that fixed v1-shaped fixtures follow the engine's
shapes; the ones for features the engine does not have yet (Update routine, pyramid sets, rest on
a wheel in exercise settings) are removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…3.10

A new pyramid_reps preset carries v1.3.10's pyramid sets: special.setReps (a whole number or
'max' per set) and special.setRest. Max rows open at what the same set managed last time, take no
finding, and do not decide whether the plan was hit; the Max flag is kept on saved rows, which
feed the record line and the Max reps chart. The migration maps a v1 pyramid exercise to the
preset (Max sets and rests included). The rule editor, MCP and plan share carry it.

Update routine is back in the exercise's more menu: warm-ups added in the session, and the rest
or note edited on its settings sheet, can be copied into the routine slot (lib/routine-update.js).
The plan's note shows on the exercise card again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
), 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
giulioleuci force-pushed the feat/generic-engine-v1.3.12-issue186 branch from ab9c77a to a5037d4 Compare October 8, 2026 07:30
@giulioleuci
giulioleuci marked this pull request as draft October 8, 2026 07:59
@giulioleuci

Copy link
Copy Markdown
Contributor Author

Hi @DuarteSantos8,
I'm making some changes to the engine, to make it more configurable and generic, and reusable in the future.

This branch has not been deployed

No deployments
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