Reconcile starter seeds on connect and add restore + duplicate cleanup
Seeding now covers existing installs, not just empty containers: after the same settle delay as auto-seed, connect() branches to reconcileSeeds(), driven by a pure tested planner — upgrade an older seed revision in place (safe: clone-on-edit guarantees no user content at seed ULIDs), skip up-to-date/quarantined files, respect delete-veto stubs, and write missing seeds unless a same-name legacy split exists. Settings gains Restore Starter Splits (the one deliberate veto lift, writing current bundle bytes) and a dev duplicate-cleanup tool backed by a fail-closed scanner. The HealthKit estimate now follows the clone redirect when resolving a workout's split. Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
@@ -69,8 +69,9 @@ All in `Shared/Model/`:
|
||||
|
||||
### Starter Data (deterministic seeds)
|
||||
- **Starter splits**: shipped as byte-canonical `SplitDocument` JSON in `Workouts/Resources/StarterSplits/*.split.json` with **fixed ULIDs** (shared `01DXF6DT00` prefix, frozen 2020 timestamp) and fixed content, regenerated only by `Scripts/generate_starter_splits.swift`. `SeedLibrary` (`Workouts/Seed/`) loads the catalog; **seeds are immutable** — `SyncEngine.save(split:)` transparently clones an edited seed to a fresh ULID and soft-deletes the seed, whose stub is exempt from pruning and vetoes resurrection forever (open views follow the identity swap via `sync.currentSplitID(for:)`).
|
||||
- **Auto-seed**: `SyncEngine.autoSeedIfEmpty()` writes the verbatim bundle bytes after connect, only into a verifiably empty container (no data files, no stubs) re-checked after a settle delay — wrong guesses are harmless because identical bytes make same-path conflicts empty and stubs reap resurrected seeds. The on-demand path (`SplitSeeder.seedDefaults`, "Add Starter Splits") restores deleted seeds (lifting the veto stub) or writes missing ones, skipping live names.
|
||||
- **Exercise library**: authored in `Exercise Library/` at the repo root (per-exercise `info.md`, SVG visuals, motion rigs, Python render pipeline); the app bundles the exported `Workouts/Resources/ExerciseMotions/*.motion.json` rigs, which double as the exercise picker's list (`ExerciseMotionLibrary.exerciseNames`).
|
||||
- **Auto-seed & reconcile**: after connect + reconcile, `SyncEngine.seedOrReconcile()` (deferred, settle-delayed) branches on container state so the two seeders can never both fire. An empty container → `autoSeedIfEmpty()` writes the verbatim bundle bytes (re-checked after the settle delay). A non-empty container → `reconcileSeeds()` diffs each bundled seed against its fixed-ULID file via the pure `SeedReconcilePlanner` (`Workouts/Sync/SeedReconcile.swift`): a stale revision is overwritten with current bundle bytes (a **semantic** compare with `schemaVersion` normalized — never a byte compare, since older files carry now-removed keys), a newly-shipped seed with no file and no stub is written (unless a live same-name split already exists, or a newer app version wrote the file), and a deleted seed (stub present) is left vetoed. Wrong guesses are harmless — identical bytes make same-path conflicts empty, stubs reap resurrected seeds, and the veto + name guards are re-checked right before each write. This is safe because a fixed-ULID file can never hold user content (every edit forks to a fresh ULID).
|
||||
- **Restore Starter Splits** (Settings) is the one deliberate veto lift: `SyncEngine.restoreSeeds()` removes a deleted seed's stub and rewrites the **current bundle bytes** (never the stub's old contents), still skipping seeds whose live file already exists (reconcile handles upgrades) or whose name collides with a live split; it returns the count for a brief in-Settings confirmation.
|
||||
- **Exercise library**: authored in `Exercise Library/` at the repo root (per-exercise `info.md`, SVG visuals, motion rigs, Python render pipeline); the app bundles the exported `Workouts/Resources/ExerciseMotions/*` resources — `*.motion.json` rigs, which double as the exercise picker's list (`ExerciseMotionLibrary.exerciseNames`), and `*.info.md` reference pages, parsed by `ExerciseInfo` and rendered in the Settings → Library → Exercises detail screen. Re-export both with `python3 render.py --export`.
|
||||
|
||||
## Guidelines
|
||||
|
||||
|
||||
Reference in New Issue
Block a user