Training Program Customization — custom splits on top of the fixed PPL (C6)
C6 replaces the fixed 6-day Push/Pull/Legs split with per-user custom training programs. Users pick from 4 starter templates (PPL 6-day / Upper-Lower 4-day / Full-body 3-day / Empty), edit days, add/remove/reorder exercises via the C3 picker, and save multiple programs with active-program switching — all with NO destructive migration (existing users keep the fixed PPL until they explicitly customize). It was the largest item in the 2026-05-31 tier carryover (RICE 7.0, 4-6 person-days estimated), and came in at ~2152 LoC across 9 standalone-buildable commits in a single ~5h session, including a cross-branch merge with sibling C3 to consume its picker-mode signature.
What do T1 / T2 / T3 mean?▾
Problem framing
| Tier | Claim |
|---|---|
| T3 | Every user followed the same fixed 6-day PPL split. Backlog L348 ("Training program customization") was filed 2026-04-02 and reaffirmed during the 2026-04-16 v5.2 stress test. Power users wanted Upper/Lower or Full-body 3-day options plus the ability to swap individual exercises. |
| T2 | Import Training Plan v1 (PR #234, 2026-05-06) shipped IMPORT-from-external CSV/JSON/Markdown but not in-app CREATE/EDIT. C6 closes the create/edit gap. |
| T1 | The 2026-05-31 E1 RICE refresh ranked C6 at #7 on the Planned table (RICE 7.0) — moderate-reach, high-impact (structural product capability), confidence 0.7, effort 1.25 person-weeks. |
What C6 shipped
- Data model (
CustomProgram→CustomDay→ExerciseSlot) whereexerciseIDis a string reference, not a copy (catalog updates flow through to all custom programs), with 3 nil-default override fields (sparse storage),schemaVersion: 1, and amaxSavedProgramsPerUser: 10soft cap. - NO destructive migration. The fixed PPL stays as a fallback constant: if
activeProgramIDis nil or points at a missing program, the resolver returns the fixed PPL safely. Existing users see unchanged behavior until they explicitly customize. - 4 starter templates (PPL 6-day / Upper-Lower 4-day / Full-body 3-day / Empty), each materializing slots from the existing
TrainingProgramData.allExercisescatalog. - 6 new surfaces — program list, template picker, the headline day-by-day editor, a day-edit sheet, a per-slot override sheet, and a Settings entry-point row.
- 8 new analytics events (all screen-prefixed
training_): list opened, template selected, saved, activated, deleted, day edited, slot added, slot removed (T1).
Cross-branch dependency on C3
C6's editor calls C3's picker-mode signature — ExerciseLibraryView(source: "picker:c6_editor", picker: { exercise in addSlot(exercise) }). C3 (PR #573) merged to main mid-C6-Phase-4; C6 then merged main, resolving the small AnalyticsProvider / AnalyticsService / pbxproj overlaps (keeping C3's shared exerciseId constant, commented for C6 reuse) and continuing with the C3 picker signature live on disk.
Verification — all green
| Check | Result |
|---|---|
xcodebuild build (generic iOS Simulator) | BUILD SUCCEEDED after every commit (T1) |
| Test suite | 15/15 PASSED on iPhone 17 Simulator (T1) |
make ui-audit | P0=0 maintained (1 pre-existing P1 on HRVTrendChart.swift, not C6-touched) (T1) |
| Tier 2.2 contemporaneous log | 6+ phase_transition entries (T1) |
The lifecycle ran Research → PRD → Tasks → Implement → Test in a single ~5h session across 9 standalone-buildable commits. Actual LoC (~2152) came in slightly higher than the PRD's ~1700 estimate (lightweight editor + per-row affordances + override-count tracking). The 15 tests cover the pure-logic surfaces (Codable round-trip, migration resolver, template materialization, analytics param shapes) at ≥90%; view-level smoke tests are deferred per the project's ViewInspector gap.
What's NOT in C6 (explicit guards)
Per-day progression curves · periodization phase blocks · AI-suggested replacements (→ D1) · shared community programs · mid-week swap warnings · bulk replacement · cross-user sharing · supersets/circuits/drop-sets — all deliberately out of scope.
Phase E discipline
C6 shipped during the v7.9 Phase E 14-day soak (2026-05-21 → ~2026-06-04). No new enforcement gates and no new schema fields beyond customPrograms[] + activeProgramID — consuming existing v7.8.6 + v7.9 infrastructure exclusively. Phase E compliant.
Source case study (FitTracker2):
docs/case-studies/training-program-customization-case-study.md. Shipped via FT2 PR #574, consuming C3's picker (PR #573). Kill-criteria first evaluation: T+60d (2026-08-01).
- •The 15 tests cover only the pure-logic surfaces (Codable round-trip, migration resolver, template materialization, analytics param shapes) at ≥90%; view-level smoke tests for the day-by-day editor are deferred per the project's ViewInspector / UI-test gap.
- •The primary metric (≥0.15 per WAU with a saved custom program) is pre-registered with baseline 0 and not first evaluated until T+60d (2026-08-01) — no adoption signal exists at ship time.
- •The "every user followed the same fixed PPL split" problem framing is a T3 narrative claim; the wall-time figure (~5h) is T2 (declared).
- Custom program save rate at T+60d < 0.05 per WAU (low adoption)
- Custom program crash rate > 1% of editor sessions (UI bug)
- p95 editor first-render > 500ms on iPhone 17 (perf)
- Migration regression: any existing user loses access to their fixed PPL
- Active-program-switch rate > 1.5/day per user (thrash signal)