Documentation

v2.5.3

GAP-Unified Pacing Design

Date: 2026-06-01 Status: Approved for planning Owner: RaceCraft

1. Problem Statement

RaceCraft currently uses two different pacing engines: - Forward mode predicts finish time from base pace plus athlete-specific modifiers. - Target-time mode solves inversely from desired finish time using a route-only anchor.

This causes user confusion because equivalent scenarios can produce different baseline pace values between modes.

The product goals are: - Exact segment parity across forward and inverse mode round-trips. - Reduced input friction. - Replace Base Pace / Natural Base Pace ambiguity with a single GAP-centric concept. - Ensure segment outputs always reflect realistic behavior (slower climbs, faster flats/descents where terrain allows).

2. Design Decisions (Locked)

  • Parity requirement: exact segment parity (within strict tolerance), not just total-time parity.
  • Input model: GAP-centric.
  • Athlete model: one simple profile tier with direct influence on segment distribution.
  • Approach selected: one canonical pacing engine shared by both modes.

3. Canonical Solver Contract

3.1 Non-negotiable behavior

A single canonical segment solver is the source of truth for both user entry paths.

  • GAP Mode:

    • Input: gap_pace + athlete_profile + route
    • Output: segment paces/times + total moving time
  • Target-Time Mode:

    • Input: target_moving_time + athlete_profile + route
    • Process: solve for gap_pace, then run the exact same canonical segment solver
    • Output: segment paces/times + solved gap_pace

Round-trip guarantee: - If GAP mode returns total moving time T, then target-time mode run with T and the same profile must return matching segment outputs within tolerance.

3.2 Canonical inputs

  • Route segments: distance, elevation gain/loss, terrain class.
  • Athlete profile: Beginner, Intermediate, Advanced (or equivalent three-tier labels).
  • Baseline speed variable: gap_pace only.
  • Optional checkpoint stops remain outside moving-time solve and are added to total race time.

3.3 Canonical outputs

  • Segment: pace, segment_time, effort label, supporting factors.
  • Summary: total moving time, total race time, estimated GAP.
  • Diagnostics: residual, iterations, clamp counts, parity signature.

3.4 Tolerances

  • Segment pace delta <= 0.01 min/km.
  • Segment time delta <= 0.1 seconds.
  • Total moving time delta <= 0.1 seconds.
  • Deterministic outputs for identical inputs.

4. Architecture and API Changes

4.1 Core backend functions

Introduce and route both modes through: - compute_segment_plan_from_gap(route_segments, gap_pace, athlete_profile, options) - solve_gap_for_target_time(route_segments, target_moving_time, athlete_profile, options)

Legacy functions can remain temporarily for migration and verification but are not final output authority post-cutover.

4.2 Request contract evolution

  • Replace primary baseline concept from z2_pace/base pace to gap_pace.
  • Keep pacing modes: gap_pace and target_time.
  • Add athlete_profile as a required field in new schema.
  • Compatibility bridge during migration:
    • Accept legacy fields (z2_pace, climbing_ability, fitness_level).
    • Map to profile defaults.
    • Emit deprecation diagnostics.

4.3 Response contract evolution

  • Keep existing segment/summary keys where possible to minimize frontend breakage.
  • Add canonical fields:
    • canonical_gap_pace
    • solver_mode
    • parity_signature
  • In target-time mode, expose solved GAP directly as first-class output.
  • Retire confusing dual baseline language in API/UI.

5. UX and Input Friction Design

5.1 Default user flow

  1. Select/upload route.
  2. Choose athlete profile (3 tiers).
  3. Choose mode (GAP Pace or Target Time).
  4. Enter one primary value (GAP or target time).
  5. Run simulation.

5.2 Terminology simplification

  • Replace Base Pace and Natural Base Pace with GAP Pace.
  • Replace Model Anchor Pace wording with Solved GAP Pace in target-time mode.
  • Keep a single plain-language helper: GAP is flat-equivalent pace; segment paces vary by route.

5.3 Profile behavior

  • Profile remains active in both modes.
  • Profile directly influences segment distribution (not bounds-only).
  • Legacy tuning controls can be moved to an optional Advanced section during transition.

5.4 Guardrails

  • Infeasible target times: show nearest feasible plan + clear reason.
  • Clamp events: concise guidance without alarmism.
  • Add a parity badge when forward/inverse parity check passes.

6. Validation, Error Handling, and Reliability

6.1 Input validation

  • GAP > 0 and within plausible domain.
  • Target moving time > 0 and valid against checkpoint-stop constraints.
  • athlete_profile in supported enum.

6.2 Solver fail-safe behavior

  • Forward solver always returns structured output or explicit error.
  • Inverse solver checks residual and convergence bounds.
  • Non-convergence returns nearest-feasible structured response (never silent mismatch).

6.3 Pace clamp handling

  • Preserve realistic min/max pace constraints.
  • If exact target is impossible under realism constraints, return feasibility-limited status explicitly.

7. Legacy Plan Migration (Critical)

7.1 Sources in scope

  • Imported JSON plans.
  • Supabase-saved plans.
  • localStorage draft/saved plans.

7.2 Versioning

  • New schema includes explicit plan_schema_version.
  • Missing/older version triggers migration pipeline.
  • Migration runs on load/import prior to calculation/save.

7.3 Required migration behavior

When legacy plan is detected: 1. Notify user that plan was upgraded to latest format. 2. Ignore deprecated fields safely. 3. Map meaningful legacy fields into canonical schema. 4. Recompute derived fields under canonical solver where required. 5. Save future updates only in new schema.

7.4 Source-specific behavior

  • JSON import: migrate in memory, show summary, load upgraded plan.
  • Supabase: migrate on read path; persist upgraded schema on next write.
  • localStorage: migrate on restore, overwrite with new schema immediately.

7.5 Migration observability

Emit migration report metadata: - from_version - to_version - mapped_fields_count - dropped_fields_count - warnings

If required essentials cannot be mapped, block with actionable error.

8. Testing Strategy

8.1 Release-blocking parity suite

For each route/profile scenario: 1. Run forward from GAP => outputs S and time T. 2. Run inverse from T => outputs S'. 3. Assert parity tolerances for segment and total values.

8.2 Coverage matrix

  • Short races.
  • Mid-distance races.
  • Ultra distances.
  • High-climb profiles.
  • Mixed/technical terrain.
  • Clamp-heavy edge cases.

8.3 Migration tests

  • Golden fixtures for legacy JSON, Supabase payloads, localStorage payloads.
  • Verify auto-migration and successful re-save in new schema.
  • Verify deprecated fields no longer affect canonical outputs.

8.4 Compatibility tests

  • Legacy API payload acceptance during migration window.
  • Stable frontend-required response fields.

9. Implementation Phasing

  1. Add canonical solver and inverse wrapper behind feature flag.
  2. Wire both modes to canonical path in calculate endpoint.
  3. Introduce GAP/profile UX defaults and terminology updates.
  4. Add migration pipeline for plan schemas (JSON/Supabase/localStorage).
  5. Enable release-blocking parity and migration test suites.
  6. Remove obsolete legacy controls and schema bridge after adoption window.

10. Success Criteria

  • Segment parity guarantee holds across both modes in test and production telemetry.
  • User-facing baseline pace ambiguity is eliminated.
  • Required pacing inputs are reduced to one primary value plus one profile.
  • Legacy plans continue to load safely and are upgraded automatically.
  • All newly saved plans use only the new schema.