Import Training Plan — Resume from Audit-Flagged Partial Ship to Full Phase 1 Ship in 14 Hours
- Version
- v7.8
- Date
- 2026-05-06
- Tier
- light
Resumed an audit-flagged partial-ship feature: rolled back to research mid-flight after discovering the original PRD claimed an impossible persistence target, rewrote the PRD against the actual write path, shipped Phase 1 (persist + activate + GDPR + 9 analytics events). Same session produced a v4.X skill-layer upgrade as a meta-byproduct: 4 new mechanical gates + auto Figma build. 4 PRs across 2 repos, 18/18 tasks, 33 new tests, 4 Figma frames auto-built (first v4.X production run), 0 P0 in ui-audit.
How to read this case studyT1/T2/T3 · ledger · kill criterion▾
- T1Instrumented
- Numbers come from a machine-generated ledger or commit. Reproducible. Highest reader trust.
- T2Declared
- Numbers stated by a structured declaration (PRD, plan, frontmatter) but not directly measured.
- T3Narrative
- Estimates and observations from session memory. Useful for context; not citable as evidence.
- Ledger
- Where to verify the claim — a file path, GitHub issue, or backlog entry. Anything labelled
ledger:is the audit trail. - Kill criterion
- The pre-registered threshold under which this work would have been killed mid-flight. Not fired = work shipped without hitting the threshold.
- Deferred
- Items intentionally not closed in this version. Each cites the ledger that tracks remaining work.
Visual aid · key numbers at a glance
Default · no specialised visual declaredThe import-training-plan feature originally shipped 2026-04-16 as a partial-ship: the parser, mapper, orchestrator, and 23 unit tests landed cleanly, but the source-picker and preview views were never wired into navigation and confirmImport() was a no-op stub. Audit UI-015 (2026-04-20) caught it. Resume attempt 2026-05-06 then surfaced a deeper structural error: the original PRD claimed ImportOrchestrator writes to TrainingProgramData — structurally impossible because TrainingProgramData is a struct with only static let fields. The honest path was a mid-flight rollback to research, a rewritten PRD against the actual persistence target (EncryptedDataStore.importedTrainingPlans), and a re-decomposed task list.
The same session also produced a v4.X skill-layer upgrade as a meta-byproduct. A user-ordered pre-Phase-4 audit caught 4 P0 spec errors that would have hit "no such symbol" at compile time (AppRadius.pill, AppMotion.standardEase, SettingsActionLabel with custom badge slot, toast component — all referenced by the spec, none in the codebase). The user requested promoting that audit pattern to a mechanical PM-workflow gate. The result: /ux preflight + /design preflight + /ux pre-merge-review + /design pre-merge-review + /design build auto-dispatch, all shipped in PR #235 and adopted by this feature on first run.
4 PRs landed across 2 repos in a single ~14-hour session · 18/44 tasks done · 33 new tests · 4 Figma frames auto-built (the v4.X auto-dispatch flow's first production output) · 0 P0 in make ui-audit · 4 P0 spec errors caught before code was written.
Architecture decisions (locked, not re-litigated)
- Persistence target =
EncryptedDataStore.importedTrainingPlans— sixth@Publishedcollection in the encrypted store, persisted via the existing 2-phase commit pattern. Closes the structural PRD gap. - Routing layer =
TrainingProgramStore— gainsactivePlanId: UUID?.exercises(for:in:)checks the flag;nilreturns bundled program, non-nilreturns the active imported plan's exercises. Mutual exclusion enforced. - Domain model =
ImportedTrainingPlan(Identifiable, Codable) — separate from the parser-transientImportedPlan. Wraps[ImportedDayAssignment](each carries the user-editableassignedDayType). - GDPR coverage — Article 17 (delete) free transitively via
EncryptedDataStore.deletePersistedData()extension. Article 20 (export) via 3 touch points inDataExportService.swift. Sync semantics deferred to Phase 2.
What shipped
| Surface | Code file | Figma node |
|---|---|---|
| Domain model | Models/ImportedTrainingPlan.swift (new) | — |
| Persistence | Services/Encryption/EncryptionService.swift (5 touch points) | — |
| Active-plan routing | Services/TrainingProgramStore.swift (4 touch points) | — |
| Orchestrator persistence | Services/Import/ImportOrchestrator.swift | — |
| GDPR Art-20 export | Services/DataExportService.swift (3 touch points) | — |
| 9 analytics events | AnalyticsProvider.swift + AnalyticsService.swift (8 new methods) | — |
| Imported Plans List screen | Views/Settings/v2/Screens/ImportedPlansListScreen.swift (new) | 919:2 populated active · 920:2 empty state |
ImportedPlanRow component | Views/Settings/v2/Components/ImportedPlanRow.swift (new) | — |
| Day-Assignment Editor | Views/Import/ImportPreviewView.swift (.preview mode extension) | 921:2 |
| Active-plan badge | Views/Training/v2/TrainingPlanView.swift (badge + toolbar Import) | 922:2 |
Outcomes (tier-tagged)
| Dimension | Pre-resume | Post-resume | Tier |
|---|---|---|---|
| Audit UI-015 status | Open (partial ship) | Closed | T1 |
| Persistence path | None (TrainingProgramData static; confirmImport() no-op) | EncryptedDataStore.importedTrainingPlans (encrypted, GDPR) | T1 |
| Active-plan switching | Impossible | TrainingProgramStore.activePlanId is the routing flag | T1 |
| Entry points | 0 (views existed but unwired) | 2 (Settings list + Training toolbar) | T1 |
| Analytics | 6 constants, 0 wired | 9 events, 100% wired through consent gate | T1 |
| Test coverage | 23 unit tests on infra | 33 new tests + 11 analytics | T1 |
| Figma↔code sync | Never built | 4 frames auto-built via v4.X /design build | T1 |
Five honest disclosures
-
The original PRD's persistence claim was structurally impossible. v1 PRD said "ImportOrchestrator writes to TrainingProgramData" — but
TrainingProgramDatais astructwith onlystatic letfields. No write path. The audit caught the partial ship; the resume caught the architectural gap. The fix was a full mid-flight rollback to research → rewritten PRD → re-decomposed tasks. -
The user-ordered pre-Phase-4 audit caught 4 P0 spec errors before any code was written. Audit cost ~20 min; Phase 4 rework cost would have been ~2-4h. This pattern is now mechanical via
/ux preflight+/design preflight(shipped in PR #235). -
The
/design buildFigma auto-dispatch was the v4.X chain's first production run. The flow worked end-to-end on first invocation: preflight → MCP liveness check → page creation → 4 mobile-screen frames → node ID write-back → state.json + figma-code-sync-status.md updates → PR description gate satisfaction. One iteration was needed (clipping fix on Frame 3). -
Three patterns net-new to the codebase.
.swipeActions,.contextMenu, and a bespokeImportedPlanRowcomponent were never used in FitMe before this feature. They're now documented infeature-memory.mdas design-system evolutions. -
Phase 2 sync is deferred. CloudKit and Supabase per-record sync for imported plans is out of Phase 1 scope. The
needsSync: Boolfield exists on the model from day one so Phase 2 can opt records in without a schema migration.
Lessons for future features
- PRDs that name persistence targets must reference the actual write path (file:line), not just the type name. v1 said "writes to TrainingProgramData"; v2 says "writes to
EncryptedDataStore.importedTrainingPlansvia the existing 2-phase commit pattern, touch points:EncryptionService.swift:779-1062(5 locations)". The latter is verifiable by Phase 6 review; the former isn't. - Pre-flight existence checks should be mechanical, not manual. The
/ux preflight+/design preflightgates added in v4.X catch this class of error before Phase 4 begins. - Figma sync is part of Phase 3, not a "deferred follow-up". Auto-dispatching
/design buildmakes Figma sync part of the contract. - Honest scope rejection beats expanding ambition mid-resume. The 1-day rollback cost was net positive.
deferredtask status is honest, not failure. Phase 2 sync is deferred with rationale and tracking — better than faking Phase 1 done with a stub.
Cross-cutting framework signals
- v7.5 / v7.6 / v7.7 / v7.8 — All write-time gates fired correctly during the resume. Zero gate skirts.
- v4.X Skill-Layer Upgrade — Shipped as a meta-byproduct. Promoted 4 audit patterns to mechanical gates. Phase 3 chain extended 7→11 steps; Phase 6 chain 4→5 steps. Phase 7 BLOCKED unless both pre-merge reviews pass.
Where things go from here
- Phase 2 follow-up PRD (out of Phase 1 scope) — CloudKit per-record sync + Supabase table + per-day editor + AI prompt regeneration + PDF/photo/share-extension sources.
- First post-launch metrics review scheduled for 2026-05-13 (T+7d) — query GA4 for
import_started→import_completed→import_plan_activatedfunnel; compute activation rate; verify kill criteria thresholds aren't tripped. - Backfill
figma_node_idsfor already-shipped features is now a normal part of the framework and will happen as features get touched.
Closing
This feature is two case studies in one. The surface case study is the persistence + active-plan + GDPR architecture closing audit UI-015. The deeper one is how the framework caught a structural PRD error mid-flight, demanded honest rework, and emerged with a v4.X skill-layer upgrade that promotes 4 audit patterns to mechanical gates so the next feature doesn't relearn the same lesson. The trigger event (4 P0 spec errors) became the test case for the new gates within the same session.
Full source case study with timeline detail, hard/easy retrospective, and per-task notes: docs/case-studies/import-training-plan-case-study.md.