Refinement of v2, integrating the user's 2026-06-20 feedback on the suit and dive schema. The structural shape of v1 and v2 is preserved. The diff is in the schema design (§3.2 in the plan) — suit is binary, start has 3 values, both are on workout_sets only.
| v2 | v3 | Why | |---|---|---| | suit_type enum: none \| jammer \| textile \| fulltech (4 values) | suit boolean, default false | "Usually one or the other, so it is quite binary" — user, 2026-06-20 | | start_type enum: in_water \| push \| dive \| backstroke_start (4 values) | start_type enum: in_water \| dive \| backstroke_start (3 values) | "In-water and a push are the same thing" — user, 2026-06-20 | | Default for start_type: not specified | Default 'in_water' | "It should be assumed that if the swim set does not have a dive or backstroke start, the swimmer is performing an in-water push start" — user, 2026-06-20 | | Fields on both workout_sets and recorded_times (denormalised) | Fields on workout_sets only; recorded_times inherits via JOIN | "If the user is wearing a suit, it should be written in the workout set" — user, 2026-06-20 |
The v3 schema is simpler, more honest about defaults, and has fewer sources of truth. The recorded_times table doesn't carry a denormalised suit flag that could drift from the set's flag — it JOINs to workout_sets at read time.
The user also surfaced a design principle: the swimset builder's job is to break a set into components, and the coach times each component separately. This is a UX principle for the swimset builder (the form where the coach defines the workout), not a schema change in this round.
The existing schema already supports this: a workout has many workout_sets rows, and each workout_set row is one component (one timed thing). The swimset builder's "break into components" UX just creates multiple workout_sets rows per logical "set" in the coach's mental model.
A future set_components table (one parent workout_set, many set_component children) is on the roadmap but is NOT in v1. The existing one-row-per-component pattern works for the Phase 1 work and avoids a structural change.
workout_sets:
suit boolean NOT NULL DEFAULT falsestart_type text NOT NULL DEFAULT 'in_water' CHECK (start_type IN ('in_water', 'dive', 'backstroke_start'))swimmer_suits (Welford, parallel to swimmer_gears):
(swimmer_id, suit) — primary key, suit is booleanmean_time_ms, stddev_time_ms, sample_count, m2, updated_atswimmer_starts (Welford, parallel to swimmer_gears):
(swimmer_id, start_type) — primary key, start_type is text enummean_time_ms, stddev_time_ms, sample_count, m2, updated_atThe pair-wise ratios:
suit: between suit=true and suit=false for the same (swimmer, stroke, distance). One delta per swimmer.start: between dive and in_water; between backstroke_start and in_water. Up to two deltas per swimmer per (stroke, distance).HistoricalTimePredictor.predict() becomes a 4-way match (swimmer, stroke, distance, intensity) + 2 calibration multipliers. The multipliers:
predicted = PB * intensity_mult * suit_mult * start_mult
sigma = max(sampleStdDev(last5), sigmaFloor) + suit_uncertainty + start_uncertainty
Where:
suit_mult = (suit=true mean at stroke/distance) / (suit=false mean at stroke/distance) — or 1.0 if the Welford hasn't accumulated ≥ 2 same-context observations under different suit values.start_mult = (dive mean at stroke/distance) / (in_water mean at stroke/distance) — or 1.0 if no calibration data yet.The 4-way match stays clean. The two multipliers are read from the Welford tables and applied as a final step. The pairing engine, the swimmer card, the SessionReviewView all benefit automatically because they consume the Prediction object.
set_components table (v2+ swimset-builder UX consideration)effort equivalence between training and racing. Suits and dive/no-dive not addressed.
as load-bearing. 4-value suit_type enum (none/jammer/textile/ fulltech), 4-value start_type enum (in_water/push/dive/ backstroke_start). Both fields on workout_sets and recorded_times (denormalised).
coaching reality. Suit is binary (most coaches are either in a suit or not, not in a 4-value taxonomy). Start collapses in_water and push. Default for start is in_water — the common case is untagged. Both fields on workout_sets only; recorded times inherit via JOIN. The "components" framing is a v2+ swimset-builder UX principle, not a schema change in v1.
The v1 → v2 move was a pivot (introduced a new concept). The v2 → v3 move was a refinement (simplified the implementation of the new concept).
brainstorm
the suit + dive adjustment introduced
(durable knowledge, may need a follow-up update for v3 schema)
the calibration is a precondition for
plans/2026-06-20_suit-and-dive-calibration-phase-1-v2 — thev2 plan with the new T1 schema
supabase/migrations/20260620000000_suit_and_dive_adjustment.sql— the migration file (drafted in this turn)
as a binary"; "in-water and a push are the same thing"; "if the swim set does not have a dive or backstroke start, the swimmer is performing an in-water push start"; "if the user is wearing a suit, it should be written in the workout set"; "crafting swim sets that you break into components so the coach can time each component is key"
using-agent-skills meta-skill (surface assumptions,push back when warranted, enforce simplicity)
incremental-implementation (vertical slices, eachshippable)
Published and managed by TARS, an AI co-author built on Nathan's gbrain.