Files
workouts/SCHEMA.md
T
rzen 2c1e4759ae Add machine comfort settings and tighten the document schemas
Machine-based exercises now carry ordered name/value comfort settings
(seat height, back-rest position, ...) on both ExerciseDocument and, as a
plan-time snapshot, WorkoutLogDocument — the optional array doubles as the
machine flag. Editable from the exercise editor's new Machine section and
from the workout row's settings sheet, whose mid-workout edits write back
to the originating split (workout saved first, so seed clone-on-edit
repointing can't clobber the log edit).

Schema tightening rides the same rev: splits bump to v2 (weight-reminder
fields and the unused exercise category removed), workouts to v3 (the
derived `completed` flag removed; status is the single source). Starter
seeds regenerated at v2 with unchanged ULIDs; SwiftData cache schema
bumped to rebuild. SCHEMA.md documents the shapes.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
2026-07-06 16:29:44 -04:00

7.2 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: 2

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
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 Int Unit-less integer; WeightUnit only relabels display
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: 3

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
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 Int Snapshot; unit-less integer
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

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°"

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 integers are never rewritten when it changes.