Hydrolyze Coach Analytics Brainstorm V4

Writing · refreshed Search related

Hydrolyze Coach Data Presentation — Brainstorm v4

Refinement of v3, documenting the schema drift discovery that forced a rewrite of the v2 migration to v3. The structural shape (per-swimmer, suit/start calibration; 4-way + 2 multipliers) is unchanged. The diff is in the schema details — the live DB has moved on from the patterns documented in the older migration files, and v3 had to match the live shape.

What changed v3 → v4

v3 (proposed)v4 (shipped)Why
swimmer_suits / swimmer_starts modeled as a Welford (mean, stddev, sample_count, m2)Modeled as a per-(swimmer, X, distance, stroke) stat (mean, stddev, sample_count, no m2); separate id PKThe live swimmer_gears (which the v2/v3 tables mirror) is a per-(swimmer, gear, distance, stroke) stat, NOT a Welford. The Welford's M2 is gone. The engine computes the running statistics from the rows directly.
RLS: swimmer_id IN (SELECT id FROM swimmers WHERE coach_id = auth.uid()::text)RLS: swimmer_id IN (SELECT id FROM swimmers WHERE coach_id IN (SELECT id FROM coaches WHERE auth_id = auth.uid()))swimmers.coach_id is uuid (FK to coaches.id), not text. The live schema has a coaches table with auth_id uuid. The obsolete text-cast pattern from 20260423000000_rls_policies.sql is broken on the live DB.
Migration file: 20260620000000_suit_and_dive_adjustment.sql (broken, never applied)Migration file: 20260620000001_suit_and_dive_adjustment_v3.sql (shipped 2026-06-20). The v2 file is preserved as an OBSOLETE no-op stub.The v2 migration failed at CREATE POLICY time (Postgres type-checks the USING expression at parse time; uuid = text fails). The v3 migration matches the live schema.

The user's feedback in v3 (binary suit, 3-value start, set-level only, "components" framing) is preserved in v4. The v3 → v4 move was a discovery, not a refinement of the design.

The schema drift discovery (v4 is the lesson)

Lesson (general, applies beyond Hydrolyze): before drafting a migration, query the live schema (information_schema.columns, pg_constraint, pg_policies) and read the actual state. Migration files on disk are design-time documents; the live DB is the implementation that counts. A migration that follows an obsolete file pattern can:

  1. Fail at parse time (as v2 did — uuid = text triggers

ERROR: 42883: operator does not exist). The migration rolls back; no changes persist; the user sees a clear error.

  1. Pass at parse time and break queries at runtime (a worse

failure mode because it's silent). The migration creates the table with a broken RLS; subsequent queries against the table return ERROR: 42883 until someone debugs.

The v2 → v3 move caught the first case (the user reported the parse-time error before any data was written). The discipline: always verify the live schema before writing migrations, not after.

The v4 schema (shipped)

workout_sets:

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

swimmer_suits (matches live swimmer_gears):

stroke text, mean_time_ms double precision, stddev_time_ms double precision, sample_count int default 0, updated_at timestamptz default now()

swimmer_starts (matches swimmer_gears):

RLS (live pattern):

swimmer_id IN (
  SELECT id FROM swimmers
  WHERE coach_id IN (
    SELECT id FROM coaches
    WHERE auth_id = auth.uid()
  )
)

The v1 → v2 → v3 → v4 design evolution (the historical record)

enum, fields on both workout_sets and recorded_times (denormalised). Welford-with-m2 schema. RLS via text-cast (obsolete).

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 (the file itself is a no-op now; running it does nothing).

swimset-builder UX consideration). The brainstorm v3 also has the wrong RLS pattern documented.

rewritten to v3 (the working migration), the migration file renamed to 20260620000001_suit_and_dive_adjustment_v3.sql (the v2 file is now a stub). The live schema was verified via information_schema queries.

The v1 → v2 → v3 moves were refinements of the design. The v3 → v4 move was a discovery (the schema drifted under the migration files), and the migration had to be rewritten to match reality.

The engine implication (unchanged from v3)

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

The multipliers are read from the new tables (one row per (swimmer, X, distance, stroke), not a Welford). The 4-way match stays clean. The pairing engine, the swimmer card, the SessionReviewView all benefit automatically.

Open questions (carried from v3)

pacing profile → CV (per the v2 §12.4 ordering).

Phase 2.

explicitly deferred.

open.

as a v2+ consideration; the existing one-row-per-component schema is sufficient for Phase 1.

See also

+ 3-value start (superseded by v4)

(matches the live schema)

— the OBSOLETE v2 file (no-op stub)

— the WORKING v3 migration (shipped 2026-06-20)

— the v3 plan (the plan file should be updated to v3 to match the v4 brainstorm and the working migration)

Source

42883: operator does not exist: uuid = text"

pg_policies) on the linked Supabase project (flidjtldfvrrntzwtnsq)

document)

push back, enforce simplicity, scope discipline, verify

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