Files
workouts/SCHEMA.md
T
rzen 394ec0989e Record per-set actuals and drive the chart and volume from them
Every completed set now writes a SetEntry (reps/weight or seconds),
pre-filled from the plan by transition(to:) so the list checkbox, both
run flows, and One More all capture for free; reset clears, skip keeps
partials. The rest and finish pages show the just-done set as a pill
that opens a stepper sheet for correcting reps and weight (2.5 lb /
1.25 kg steps). The Weight Progression chart plots the top-set actual
weight and workout volume sums recorded sets, both falling back to the
plan for legacy logs via effectiveSetEntries.

Storage side of UX #3 rides along: plan weights are Double now.
Schema bumps: SplitDocument 2→3, WorkoutDocument 3→4 (a fractional
weight fails an older Int decode, and a rewrite would strip the
irreplaceable actuals), SwiftData cache 4→5. A per-log updatedAt is
reserved for the future cross-device log merge.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
2026-07-08 12:48:37 -04:00

8.6 KiB
Raw Blame History

SCHEMA.md

The on-disk JSON document schemas — the app's source of truth. The canonical definitions are the Codable structs in Shared/Model/Documents.swift (enums in Shared/Model/Enums.swift); this file is a readable summary. If they disagree, the code wins.

Encoding (IndieSync DocumentCoder): pretty-printed JSON with sorted keys; dates as ISO-8601 strings. One file per aggregate root:

Aggregate Path in iCloud container
Split (embeds its exercises) Splits/<ULID>.json
Workout (embeds its logs) Workouts/YYYY/MM/<ULID>.json (local-calendar month of start)
Tombstone (soft-delete stub) Stubs/<same relative path as the deleted file>

schemaVersion gates forward compatibility (VersionedDocument): files written by a newer app version are quarantined, not rewritten. Fields marked not schema-bumped were added as optionals without a version bump — an older app silently drops them on rewrite, which is accepted as preferable to quarantine.

The same shapes are also the iPhone↔Watch wire format, and each has a mirrored SwiftData cache entity (Shared/Model/Entities.swift) kept in sync by Mappers.swift.


SplitDocument — currentSchemaVersion: 3

Property Type Optional Notes
schemaVersion Int Forward-compatibility gate. Bumped 1→2 when the weight-reminder fields and category were removed from ExerciseDocument and machineSettings was added — removing required fields means older apps can't decode new files, so they must quarantine, not rewrite. Bumped 2→3 when ExerciseDocument.weight went Int → Double — an older app decoding a fractional weight into Int fails, so it must quarantine
id String ULID
name String
color String Theme color name
systemImage String SF Symbol name
order Int Sort position in the splits list
createdAt Date
updatedAt Date
exercises [ExerciseDocument] Embedded aggregate members
activityType Int Raw WorkoutActivityType; nil → traditionalStrength. Not schema-bumped

ExerciseDocument (embedded in SplitDocument)

Property Type Optional Notes
id String ULID
name String Also keys the motion rig / picker catalog
order Int Sort position within the split
sets Int
reps Int
weight Double Unit-less; WeightUnit only relabels display. Int → Double in schema v3 (old integer values decode natively)
loadType Int Raw LoadType
durationSeconds Int Total seconds; 0 when not a timed exercise
machineSettings [MachineSetting] Ordered machine comfort settings; nil → not a machine exercise, empty → machine exercise with nothing recorded. Doubles as the machine-based flag

WorkoutDocument — currentSchemaVersion: 4

Property Type Optional Notes
schemaVersion Int Bumped 1→2 when metrics was added (captured HR data is irreplaceable, so older apps must quarantine, not strip). Bumped 2→3 when the derived completed flag was removed from WorkoutLogDocument and machineSettings was added — dropping a required field means older apps can't decode new files, so they must quarantine. Bumped 3→4 when WorkoutLogDocument.weight went Int → Double and per-set actuals (setEntries) were added — a fractional weight fails an older decode, and a rewrite would strip the irreplaceable actuals
id String ULID (chronological — drives month bucketing and sorting)
splitID String Denormalized reference; no live relationship
splitName String Denormalized snapshot of the split's name
start Date Also determines the file's YYYY/MM bucket (never re-bucketed after write)
end Date Stamped by recomputeStatusFromLogs() when all logs resolve
status String Raw WorkoutStatus; derived from logs, never set directly
createdAt Date
updatedAt Date
logs [WorkoutLogDocument] Embedded aggregate members
metrics WorkoutMetrics Nil until the workout completes and a writer fills it in

WorkoutLogDocument (embedded in WorkoutDocument)

Property Type Optional Notes
id String ULID
exerciseName String Snapshot by name — no reference to the exercise's ULID
order Int Sort position within the workout
sets Int Planned sets (snapshot from the exercise)
reps Int Planned reps (snapshot)
weight Double Snapshot; unit-less. Int → Double in schema v4 (old integer values decode natively)
loadType Int Raw LoadType (snapshot)
durationSeconds Int Total seconds; 0 when not a timed exercise
currentStateIndex Int Progress through the set sequence
status String Raw WorkoutStatus; mutate only via transition(to:). Completion is derived: completed == (status == "completed")
notes String
date Date
machineSettings [MachineSetting] Snapshot of the exercise's machine settings at plan time; nil → not a machine exercise, empty → nothing recorded
startedAt Date First move to .inProgress. Not schema-bumped
completedAt Date Last move to .completed. Not schema-bumped
setEntries [SetEntry] Per-set actuals in set order; nil → legacy file / nothing recorded. Appended as sets complete (pre-filled from the plan); transition(to:) fills the missing tail on .completed and clears on .notStarted; .skipped keeps partials. Added in schema v4
updatedAt Date Reserved for the future per-log merge (H1); nothing writes it yet. Added in schema v4

MachineSetting (embedded in ExerciseDocument and WorkoutLogDocument)

A single free-form comfort setting for a machine-based exercise. value is a string, not a number, because machine dials aren't uniformly numeric.

Property Type Optional Notes
name String e.g. "Seat Height", "Back Rest Incline"
value String Free-form, e.g. "4", "3rd hole", "45°"

SetEntry (embedded in WorkoutLogDocument, schema v4)

One performed set. Which fields are set follows the log's LoadType: weight → reps + weight; none (bodyweight) → reps; duration → seconds.

Property Type Optional Notes
reps Int
weight Double Unit-less, like the plan snapshot
seconds Int Held/worked seconds for duration exercises
completedAt Date When the set finished

WorkoutMetrics (embedded in WorkoutDocument, schema v2)

Property Type Optional Notes
activeEnergyKcal Double
avgHeartRate Double bpm; watch sessions only
maxHeartRate Double bpm; watch sessions only
minHeartRate Double bpm; watch sessions only
totalVolume Double Σ sets×reps×weight across weighted logs
hrZoneSeconds [Double] Seconds in each of 5 HR zones, low→high
healthKitWorkoutUUID String Presence is the phone/watch Health-write dedupe signal
source String Raw MetricSource
recordedAt Date

Tombstone (IndieSync, minimal stub)

A delete writes a stub to Stubs/…, then removes the live file. A full stub is a verbatim copy of the record's JSON plus an injected deletedAt; the minimal stub below is the fallback when the source is unreadable. Stubs are pruned after a 30-day grace period, except starter-seed stubs (exempt, and they veto seed resurrection forever).

Property Type Optional Notes
id String The deleted record's ULID
deletedAt Date
kind String App-defined category

Enum raw values

WorkoutStatus (String — on WorkoutDocument.status, WorkoutLogDocument.status): notStarted, inProgress, completed, skipped

LoadType (Int — on ExerciseDocument.loadType, WorkoutLogDocument.loadType): 0 none · 1 weight · 2 duration

WorkoutActivityType (Int — on SplitDocument.activityType): 0 traditionalStrength · 1 functionalStrength · 2 hiit · 3 coreTraining · 4 cardio · 5 cycling

MetricSource (String — on WorkoutMetrics.source): watch, phoneEstimate

WeightUnit (lb/kg) is a display setting only — it is never persisted in documents, and stored weight values are never rewritten when it changes.