Suit And Dive Adjustment

Concept Search related

Suit & Dive Adjustment (swim coaching data quality) — v4

v4 reflects the live schema (2026-06-20) + the T1.5 m2 column.

The T1.5 migration (20260620000002_add_m2_column_to_calibration_tables.sql) added the Welford M2 accumulator to both swimmer_suits and swimmer_starts. Without M2, the engine cannot do O(1) online Welford updates; recomputing from the full recorded_times history on every save is O(n) per write and loses precision versus the canonical incremental formula.

The Welford m2 rationale (the precision argument)

For each (swimmer, X, distance, stroke) row, the engine maintains:

The Welford online update formula is the canonical way to maintain running mean and variance in O(1) per observation:

count_new = count_old + 1
delta    = x_new - mean_old
mean_new = mean_old + delta / count_new
M2_new   = M2_old + delta * (x_new - mean_new)
variance = M2 / (count - 1)
stddev   = sqrt(variance)   # cached into stddev_time_ms

Without M2 stored, the engine cannot do incremental updates. It would have to recompute mean and variance from the full history of recorded_times for that (swimmer, X, distance, stroke) bucket on every save — O(n) per write, with precision drift as the sum gets large. The standard Welford is the answer; M2 is the state we need to keep.

The column is hidden (engine-only). The coach never sees m2 in the UI. The engine reads/writes m2 on every recorded_times insert for the matching bucket. stddev_time_ms stays in the table as a cached value (derived from m2 on each update) for backwards compat with the live swimmer_gears shape.

Schema drift discovery (2026-06-20, unchanged from v3)

When I tried to apply the v2 migration, it failed with:

ERROR: 42883: operator does not exist: uuid = text
HINT:  No operator matches the given name and argument types.
You might need to add explicit type casts.

The error came from the CREATE POLICY in v2's RLS, which used coach_id = auth.uid()::text. The live swimmers.coach_id is uuid (FK to coaches.id), not text. The migration file I read (20260423000000_rls_policies.sql) had the obsolete text-cast pattern; the live DB had been migrated past it. The v3 plan and v3 migration file corrected this by using the live 2-level join pattern (swimmer_id → swimmers.coach_id → coaches.auth_id = auth.uid()). T1 shipped on 2026-06-20.

Lesson (general, not just for Hydrolyze): before drafting a migration, query the live schema (information_schema.columns, pg_constraint, pg_policies) and read the actual state. Migration files are design-time documents; the live DB is the implementation that counts.

The live schema (v4 final)

workout_sets:

CHECK (start_type IN ('in_water', 'dive', 'backstroke_start'))

swimmer_suits (matches the live swimmer_gears shape + v4 m2):

swimmer_starts (matches swimmer_gears + v4 m2):

RLS (live pattern, both tables):

CREATE POLICY "coach_own_swimmer_suits" ON swimmer_suits
  FOR ALL USING (
    swimmer_id IN (
      SELECT id FROM swimmers
      WHERE coach_id IN (
        SELECT id FROM coaches
        WHERE auth_id = auth.uid()
      )
    )
  );

The engine implication (v4)

HistoricalTimePredictor.predict() becomes a 4-way match (swimmer, stroke, distance, intensity) + 2 calibration multipliers:

predicted = PB * intensity_mult * suit_mult * start_mult
sigma     = max(sampleStdDev(last5), sigmaFloor) + suit_uncertainty + start_uncertainty

Where:

The Welford write path (T6, T7 in the v3 plan) updates the tables on every recorded_times insert using the formula above. The m2 column is the source of truth for variance; stddev_time_ms is the cached value. Without m2, the engine would have to recompute the mean and stddev from full history on every save — O(n) per write and losing precision.

The McEvoy reference (unchanged from v1/v2/v3)

Per the existing people/cameron-mcevoy page:

post-2010 FINA rules, no polyurethane). His competition PB is 20.88 in textile. The Cielo record (20.91) was in a tech suit; the difference is ~0.15% — at the limit of detectability from a single race. So for sprint textile racing today, the suit factor is small.

interview). Hydrolyze's coach-cohort won't. The training band for an unsuit-trained swimmer is shifted relative to McEvoy's suit-trained band. The prediction engine has to handle both.

a 5.32 m/s best (consistent 5.2). His 10m time dropped from 3.4s to sub-3.1s. The dive is a real, trainable, measurable component.

v1 → v2 → v3 → v4 evolution (design)

text-cast pattern. Never applied.

set-level only. RLS still inherited the text-cast pattern. The migration file 20260620000000_suit_and_dive_adjustment.sql is preserved as an OBSOLETE stub.

drift discovered; live schema is per-(swimmer, X, distance, stroke) stat (not Welford), with 2-level join RLS. T1 shipped with the corrected schema.

Welford updates. The schema is now complete: the engine has all the state it needs to do O(1) incremental updates on every recorded_times insert.

See also

the calibration is a precondition for

calibration that this sits alongside

design evolution with the schema-drift discovery

brainstorm (binary suit, 3-value start; superseded by v4)

brainstorm (4-value enum; superseded)

brainstorm (original)

sibling intensity calibration

— the v2 file (OBSOLETE stub, no-op)

— the v3 file (T1, shipped 2026-06-20)

— the T1.5 file (m2 column, shipped 2026-06-20)

— the engine file to extend

— to be created in T2

STALE migration file (obsolete text-cast pattern); do not trust for live-schema work

Published and managed by TARS, an AI co-author built on Nathan's gbrain.