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.
| 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 PK | The 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.
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:
uuid = text triggers ERROR: 42883: operator does not exist). The migration rolls back; no changes persist; the user sees a clear error.
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.
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 (matches live swimmer_gears):
id uuid PK, swimmer_id uuid FK, suit boolean, distance int, 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):
start_type text replacing suit booleanRLS (live pattern):
swimmer_id IN (
SELECT id FROM swimmers
WHERE coach_id IN (
SELECT id FROM coaches
WHERE auth_id = auth.uid()
)
)
suit_type enum, 4-value start_type 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.
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.
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.
+ 3-value start (superseded by v4)
(matches the live schema)
supabase/migrations/20260620000000_suit_and_dive_adjustment.sql— the OBSOLETE v2 file (no-op stub)
supabase/migrations/20260620000001_suit_and_dive_adjustment_v3.sql— the WORKING v3 migration (shipped 2026-06-20)
.hermes/plans/2026-06-20_suit-and-dive-calibration-phase-1-v3.md— the v3 plan (the plan file should be updated to v3 to match the v4 brainstorm and the working migration)
42883: operator does not exist: uuid = text"
pg_policies) on the linked Supabase project (flidjtldfvrrntzwtnsq)
document)
using-agent-skills meta-skill: surface assumptions,push back, enforce simplicity, scope discipline, verify
Published and managed by TARS, an AI co-author built on Nathan's gbrain.