Files
workouts/Scripts/generate_starter_splits.swift
T
rzen 669ecf1259 Surface machine settings in the exercise library and refine machine rigs
Library detail screens now lead with the user's recorded machine settings
(per-split when they disagree, empty-state card for machine-based entries)
and append the weight progression chart. Starter seeds mark machine
exercises with an empty machineSettings list so the settings UI lights up
before first use. The figure rig gains a frontal body profile for face-on
machines, props that can ride mid joints (knees/elbows), and an
alternating four-frame Bird Dog loop.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
2026-07-06 18:35:15 -04:00

143 lines
5.6 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: Int
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 = 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: "Bird Dog", sets: 3, reps: 8, load: 0),
Ex(name: "Reverse Crunch", sets: 3, reps: 12, load: 0),
]),
]
// 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: 2,
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)")
}