Files
workouts/Scripts/generate_starter_splits.swift
T
rzen 60927b5d1f Add 17 warm-up, stretch, and mobility exercises with new starter splits
The library grows to 64 exercises: arm circles, torso twist, leg
swings, hip and neck mobility, marching, calf raises, and a full
stretching set (forward fold, quad, calf, chest, triceps, hip flexor,
butterfly, cobra, child's pose), each with an authored motion rig and
reference page. Eight new starter splits join the catalog — Upper and
Lower Body Warm-Up, Morning Wake-Up, Morning Mobility, Full Body
Stretch, Evening Stretch, Free Weight Basics, and Full Body Machines —
regenerated deterministically at split schema v3 with fixed ULIDs.

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

210 lines
9.8 KiB
Swift

// Generates the bundled starter-split JSON resources for Workouts.
// Deterministic: fixed ULID timestamp (2020-01-01T00:00:00Z) + SHA256-of-label
// randomness, fixed document timestamps, encoder matching DocumentCoder
// (prettyPrinted + sortedKeys + iso8601). Re-running always emits identical bytes.
import Foundation
import CryptoKit
// MARK: - ULID (Crockford base32, 48-bit ms timestamp + 80 deterministic bits)
let crockford = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
func deterministicULID(label: String) -> String {
let timestampMS: UInt64 = 1_577_836_800_000 // 2020-01-01T00:00:00Z
var bytes = [UInt8]()
for shift in stride(from: 40, through: 0, by: -8) {
bytes.append(UInt8((timestampMS >> UInt64(shift)) & 0xFF))
}
let digest = SHA256.hash(data: Data(label.utf8))
bytes.append(contentsOf: Array(digest).prefix(10))
precondition(bytes.count == 16)
// Encode 128 bits as 26 Crockford base32 chars: big-int division by 32, 26 digits.
var chars = [Character]()
var num = bytes
for _ in 0..<26 {
var remainder = 0
var quotient = [UInt8]()
for byte in num {
let acc = remainder * 256 + Int(byte)
quotient.append(UInt8(acc / 32))
remainder = acc % 32
}
chars.append(crockford[remainder])
num = quotient
}
return String(chars.reversed())
}
// MARK: - Document shapes (field names must mirror Shared/Model/Documents.swift)
struct MachineSetting: Codable {
var name: String
var value: String
}
struct ExerciseDocument: Codable {
var id: String
var name: String
var order: Int
var sets: Int
var reps: Int
var weight: Double
var loadType: Int
var durationSeconds: Int
var machineSettings: [MachineSetting]? = nil
}
struct SplitDocument: Codable {
var schemaVersion: Int
var id: String
var name: String
var color: String
var systemImage: String
var order: Int
var createdAt: Date
var updatedAt: Date
var exercises: [ExerciseDocument]
var activityType: Int?
}
// MARK: - Seed data (canonical source for Workouts/Resources/StarterSplits/*.json)
// `machine: true` emits `machineSettings: []` — marks the exercise machine-based
// (empty = nothing recorded yet), which lights up the machine-settings UI without
// the user having to flip the editor toggle first.
struct Ex { let name: String; var weight: Double = 0; var sets = 4; var reps = 10; var load = 1; var dur = 0; var machine = false }
struct Sp { let name: String; let color: String; let icon: String; let activity: Int; let ex: [Ex] }
let seeds: [Sp] = [
Sp(name: "Upper Body", color: "blue", icon: "figure.strengthtraining.traditional", activity: 0, ex: [
Ex(name: "Lat Pull Down", weight: 110, machine: true),
Ex(name: "Seated Row", weight: 90, machine: true),
Ex(name: "Shoulder Press", weight: 40, machine: true),
Ex(name: "Chest Press", weight: 40, machine: true),
Ex(name: "Tricep Press", weight: 100, machine: true),
Ex(name: "Arm Curl", weight: 40, machine: true),
]),
Sp(name: "Core", color: "orange", icon: "figure.core.training", activity: 3, ex: [
Ex(name: "Abdominal", weight: 40, machine: true),
Ex(name: "Rotary", weight: 40, machine: true),
]),
Sp(name: "Lower Body", color: "green", icon: "figure.run", activity: 0, ex: [
Ex(name: "Leg Press", weight: 140, machine: true),
Ex(name: "Leg Curl", weight: 40, machine: true),
Ex(name: "Leg Extension", weight: 80, machine: true),
Ex(name: "Abductor", weight: 130, machine: true),
Ex(name: "Adductor", weight: 130, machine: true),
Ex(name: "Calfs", weight: 100, machine: true),
]),
Sp(name: "Bodyweight Core", color: "teal", icon: "figure.core.training", activity: 3, ex: [
Ex(name: "Cat-Cow", sets: 1, reps: 10, load: 0),
Ex(name: "Dead Bug", sets: 1, reps: 8, load: 0),
Ex(name: "Bird Dog", sets: 1, reps: 8, load: 0),
Ex(name: "Plank", sets: 3, reps: 0, load: 2, dur: 45),
Ex(name: "Hollow Body Hold", sets: 3, reps: 0, load: 2, dur: 30),
Ex(name: "Side Plank", sets: 3, reps: 0, load: 2, dur: 30),
Ex(name: "Leg Raises", sets: 3, reps: 12, load: 0),
Ex(name: "Crunch", sets: 3, reps: 12, load: 0),
Ex(name: "Reverse Crunch", sets: 3, reps: 12, load: 0),
]),
Sp(name: "Full Body Machines", color: "purple", icon: "figure.strengthtraining.traditional", activity: 0, ex: [
Ex(name: "Leg Press", weight: 100, machine: true),
Ex(name: "Chest Press", weight: 40, machine: true),
Ex(name: "Seated Row", weight: 70, machine: true),
Ex(name: "Shoulder Press", weight: 30, machine: true),
Ex(name: "Lat Pull Down", weight: 80, machine: true),
Ex(name: "Abdominal", weight: 40, machine: true),
]),
Sp(name: "Free Weight Basics", color: "red", icon: "dumbbell.fill", activity: 0, ex: [
Ex(name: "Goblet Squat", weight: 25, sets: 3, reps: 10),
Ex(name: "Dumbbell Bench Press", weight: 25, sets: 3, reps: 8),
Ex(name: "Dumbbell Row", weight: 30, sets: 3, reps: 8),
Ex(name: "Dumbbell Shoulder Press", weight: 20, sets: 3, reps: 8),
Ex(name: "Romanian Deadlift", weight: 65, sets: 3, reps: 8),
Ex(name: "Dumbbell Curl", weight: 20, sets: 3, reps: 10),
]),
Sp(name: "Upper Body Warm-Up", color: "cyan", icon: "figure.arms.open", activity: 1, ex: [
Ex(name: "Arm Circles", sets: 2, reps: 10, load: 0),
Ex(name: "Torso Twist", sets: 2, reps: 10, load: 0),
Ex(name: "Standing Side Bend", sets: 2, reps: 8, load: 0),
Ex(name: "Cat-Cow", sets: 1, reps: 8, load: 0),
Ex(name: "Push-Up", sets: 2, reps: 8, load: 0),
]),
Sp(name: "Lower Body Warm-Up", color: "mint", icon: "figure.walk", activity: 1, ex: [
Ex(name: "March in Place", sets: 1, reps: 0, load: 2, dur: 60),
Ex(name: "Leg Swings", sets: 2, reps: 10, load: 0),
Ex(name: "Hip Circles", sets: 2, reps: 8, load: 0),
Ex(name: "Bodyweight Squat", sets: 2, reps: 12, load: 0),
Ex(name: "Glute Bridge", sets: 2, reps: 10, load: 0),
Ex(name: "Standing Calf Raise", sets: 2, reps: 12, load: 0),
]),
Sp(name: "Morning Wake-Up", color: "yellow", icon: "sun.max.fill", activity: 1, ex: [
Ex(name: "March in Place", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Neck Rolls", sets: 1, reps: 5, load: 0),
Ex(name: "Arm Circles", sets: 1, reps: 10, load: 0),
Ex(name: "Torso Twist", sets: 1, reps: 10, load: 0),
Ex(name: "Standing Side Bend", sets: 1, reps: 8, load: 0),
Ex(name: "Hip Circles", sets: 1, reps: 8, load: 0),
Ex(name: "Leg Swings", sets: 1, reps: 10, load: 0),
Ex(name: "Standing Calf Raise", sets: 1, reps: 12, load: 0),
Ex(name: "Standing Forward Fold", sets: 1, reps: 0, load: 2, dur: 30),
]),
Sp(name: "Morning Mobility", color: "pink", icon: "figure.yoga", activity: 1, ex: [
Ex(name: "Cat-Cow", sets: 1, reps: 10, load: 0),
Ex(name: "Bird Dog", sets: 1, reps: 8, load: 0),
Ex(name: "Dead Bug", sets: 1, reps: 8, load: 0),
Ex(name: "Glute Bridge", sets: 1, reps: 12, load: 0),
Ex(name: "Cobra Stretch", sets: 1, reps: 8, load: 0),
Ex(name: "Child's Pose", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Standing Forward Fold", sets: 1, reps: 0, load: 2, dur: 30),
]),
Sp(name: "Full Body Stretch", color: "brown", icon: "figure.flexibility", activity: 1, ex: [
Ex(name: "Standing Forward Fold", sets: 1, reps: 0, load: 2, dur: 30),
Ex(name: "Standing Quad Stretch", sets: 2, reps: 0, load: 2, dur: 30),
Ex(name: "Standing Calf Stretch", sets: 2, reps: 0, load: 2, dur: 30),
Ex(name: "Chest Opener", sets: 1, reps: 0, load: 2, dur: 30),
Ex(name: "Overhead Triceps Stretch", sets: 2, reps: 0, load: 2, dur: 30),
Ex(name: "Kneeling Hip Flexor Stretch", sets: 2, reps: 0, load: 2, dur: 30),
Ex(name: "Child's Pose", sets: 1, reps: 0, load: 2, dur: 45),
]),
Sp(name: "Evening Stretch", color: "indigo", icon: "figure.mind.and.body", activity: 1, ex: [
Ex(name: "Cat-Cow", sets: 1, reps: 10, load: 0),
Ex(name: "Butterfly Stretch", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Kneeling Hip Flexor Stretch", sets: 2, reps: 0, load: 2, dur: 30),
Ex(name: "Cobra Stretch", sets: 1, reps: 8, load: 0),
Ex(name: "Child's Pose", sets: 1, reps: 0, load: 2, dur: 45),
]),
]
// MARK: - Emit
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.dateEncodingStrategy = .iso8601
let fixedDate = ISO8601DateFormatter().date(from: "2020-01-01T00:00:00Z")!
let outDir = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
try FileManager.default.createDirectory(at: outDir, withIntermediateDirectories: true)
for (order, sp) in seeds.enumerated() {
let slug = sp.name.lowercased().replacingOccurrences(of: " ", with: "-")
let doc = SplitDocument(
schemaVersion: 3,
id: deterministicULID(label: "starter.\(slug)"),
name: sp.name, color: sp.color, systemImage: sp.icon, order: order,
createdAt: fixedDate, updatedAt: fixedDate,
exercises: sp.ex.enumerated().map { idx, e in
ExerciseDocument(
id: deterministicULID(label: "starter.\(slug).ex\(idx)"),
name: e.name, order: idx, sets: e.sets, reps: e.reps, weight: e.weight,
loadType: e.load, durationSeconds: e.dur,
machineSettings: e.machine ? [] : nil)
},
activityType: sp.activity)
let data = try encoder.encode(doc)
let file = outDir.appendingPathComponent("\(sp.name).split.json")
try data.write(to: file)
print("\(doc.id) \(sp.name) (\(sp.ex.count) exercises)")
}