Repository navigation
feat(engine): generic prescription engine v2, smart warm-ups and v1→v2 migration - #312
giulioleuci wants to merge 12 commits into
Conversation
109866a to
e747c3a
Compare
|
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 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. |
|
Hi @DuarteSantos8!
|
48722e9 to
b81d3de
Compare
7d709f8 to
b793105
Compare
…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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw
… 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
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw
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
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>
ab9c77a to
a5037d4
Compare
|
Hi @DuarteSantos8, |
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/)hold_secondspreset for bodyweight holds that climb in seconds.progression.jsandfinish-workout.jsare removed;finish-session.jsreads v2 work sets only (PRs and best weights ignore warm-ups).Intelligent warm-ups (#156)
Data migration
MigrationGate).Consumers moved to v2
Migration audit and hardening (follow-up commits)
An independent audit of the v1 → v2 migration against v1's own
nextPrescription(findings and reproductions inREPORT.md), then a seeded fuzz and edge-case suite. Result: no v1 document can leave the migration stuck.api/migration/profile-pack.js), about half the size, so heavy users stay under the 16 MB sync cap.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 inmigrationAudit; a missingprofile-validation.jsis now tracked.api/test/migration-robustness.test.jschecks, 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.jswidens the search.REPORT.md§9.Tests: API 633, frontend 3305, MCP 29 — all passing.
Docs
docs/andmcp/aligned with the v2 engine and smart warm-ups.🤖 Generated with Claude Code
https://claude.ai/code/session_018WPFKTEHhsh3gknUQqKzSw