Documentation

Flat Terrain Race Pace and GAP Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [x]) syntax for tracking.

Goal: Explain manual race pace, distinguish grade-only GAP from the required manual input, and guide athletes through a predictable round trip.

Architecture: Preserve both segment engines and legacy API fields. Add explicit Target Time metrics and a fixed-default forward solve; use a small browser helper for the active target reference and settings comparison.

Tech Stack: Python/Flask, vanilla JavaScript, existing CSS, unittest and Node.

Spec: docs/superpowers/specs/2026-10-26-flat-race-pace-and-gap-design.md

Global Constraints

  • Default baseline: terrain on, skill 0.5 (Strong), fatigue on, Recreational fitness, wind on, Mixed exposure.
  • Target Time's baseline must ignore previous manual and athlete-profile values.
  • Keep base_pace, z2_pace and legacy response/save fields compatible.
  • Extract all copy into locales/en-AU.json; use CSS tokens and existing disclosures.
  • Use real race GPX fixtures. Preserve unrelated test_pacing_mode_parity.py.
  • Work on the existing feature branch requested throughout this conversation; no release or deployment.

Review Focus

  • Legacy saved plans without the additive metrics must never display the old anchor as grade-only GAP (Task 3).
  • A route, terrain or checkpoint edit must not leave stale round-trip advice (Task 2).
  • Changing hidden manual inputs, including pace, must not influence the Target Time reference through wind scheduling (Task 1).
  • An unreachable target must not claim that its bounded required pace reproduces the total (Tasks 1-2).
  • Imperial pace entry and inactive dependent controls must not produce false settings mismatches (Task 2).

Task 1: Distinct Target Time metrics and deterministic defaults

Files: modify app.py; create tests/test_target_pacing_metrics.py.

Interfaces: - grade_adjusted_plan_pace(segments, moving_times) -> float: unrounded grade-only pace in min/km. - Add summary grade_adjusted_pace, grade_adjusted_pace_str for Target Time only. - Add pacing_reference: required_flat_pace, required_flat_pace_str, settings, status (matched, bounded, unavailable), wind_available. - Preserve summary.estimated_gap and its current compatibility semantics.

  • Write failing tests: real-route GAP uses moving time only; pure climb is faster-equivalent and runnable descent slower-equivalent; rolling gain/loss does not cancel. Fixed-default required pace remains equal under opposite manual settings. Displayed pace round-trips on UTA50/UTA100 within the half-second-per-kilometre forward sensitivity plus response precision. Unreachable target returns bounded. Stub forecast transport for stable wind and assert totals, including differing hidden manual pace.
  • Run python -m unittest tests.test_target_pacing_metrics; confirm missing additive fields/behaviour fails.
  • Implement grade-only aggregation using distance_weighted_gap_factor. Accumulate unrounded segment moving times. Solve required pace separately with fixed defaults; bind wind to the candidate's still-air forward schedule, reusing the loaded forecast rather than fetching per candidate. Determine reachability from the forward residual. Exclude checkpoint stops from the solve.
  • Run the new tests plus tests.test_gap_flat_pace_roundtrip and tests.test_target_time_display; all must pass.

Task 2: Active-reference comparison and subtle helper

Files: create static/js/pacing-guidance.js, tests/test_pacing_guidance.py; modify static/js/app.js.

Interfaces: - Browser/CommonJS PacingGuidance.createReference(reference, planInputs) copies only the active result and explicit route/checkpoint inputs. - PacingGuidance.compare(reference, planInputs) returns {status, changes} using stable setting identifiers, without translated text. - Rendering resolves localisation keys in app.js; the helper never changes inputs or stores history.

  • Write Node-backed unittest cases asserting matching inputs produce matched, fatigue/terrain/skill/wind changes identify only relevant settings, stops invalidate equivalence, route/terrain edits require recalculation, bounded targets never match, and a new target replaces the old reference. Pace is compared in internal min/km with whole-second display tolerance, including imperial conversion.
  • Run python -m unittest tests.test_pacing_guidance; confirm failure before implementation.
  • Implement the comparison helper. Capture target references after successful current calculation responses; compare live inputs on panel edits, including mode changes; clear reference on plan reset/load/import/new route. Include race date/start/exposure as wind conditions, and distinguish forecast caveats from settings mismatches.
  • Run python -m unittest tests.test_pacing_guidance; all must pass.

Task 3: Clear labels and separate result presentation

Files: modify templates/index.html, static/js/app.js, static/css/style.css, locales/en-AU.json, tests/test_header_navigation.py, tests/test_target_time_details_toggle.py.

Interfaces: consume Task 1's additive metrics and Task 2's comparison result. Use a muted pacing-roundtrip-note element in the Pacing panel with polite status updates.

  • Add behaviour assertions for accessible explanatory text and default terrain on. Extend the Node harness to render the distinct Target Time values, hide manual GAP results, and leave legacy GAP unavailable. Preserve disclosure expansion across recalculation.
  • Run affected tests and observe new behaviour fail.
  • Add manual label Flat Terrain Race Pace and the spec's visible explanation; mode/guidance become Flat Terrain Pace. Default new plans' terrain toggle/state to on. Show grade-only GAP in Target Time's summary; replace the detail's old model-anchor row with Required Flat Terrain Race Pace. Remove repeated manual output. Translate helper messages for settings, matching, changed plan, unavailable/bounded and rounding/forecast qualifications. Preserve explicit manual/saved-plan values.
  • Run Node behaviour tests, navigation/details tests and tests.test_i18n; all must pass. Run node --check for changed JavaScript.

Task 4: Documentation and complete verification

Files: update docs/3. models/Climbing Model.md and relevant pacing explanations in existing docs.

  • Document grade-only GAP, required manual pace, defaults and total-only round-trip limits; remove claims that a manual input is a separately calculated GAP.
  • Run python -m unittest discover -s tests -p "test_*.py", python -m ruff check ., and Ruff format check on tracked Python files. Fix findings with Ruff.
  • Run before/after Snyk code and dependency scans for touched calculation inputs. Attempt Docker build and report any environment blocker accurately.
  • Review the whole diff against the approved design, using one fresh reviewer, address material findings with regression tests, and record verification.
  • Commit only task-owned files with a Conventional Commit; leave the unrelated untracked test alone.

Verification and review

  • Full suite: 753 tests passed. Ruff check passed; 91 Python files passed format checking. Both changed JavaScript modules passed syntax checking; diff whitespace checking passed.
  • Docker image racecraft-pacing-review built successfully.
  • Independent review found four issues, all fixed with failing-then-passing regression checks: imperial pace double rounding, reference capture after an in-flight edit, checkpoint drift on unit conversion, and incorrect rejection of valid manual paces above 20:00. The last was promoted from minor because it incorrectly denies an achievable round trip. Exact 20:59 and out-of-range cases are covered. No review findings are deferred.
  • Snyk Code: 11 existing findings (0 high, 8 medium, 3 low); dependencies: 18 existing findings. The medium Code baseline is tracked in #578, dependencies in #633, and the three low triage items in #640. Escaped weather markup and same-origin language redirects explain the medium false-positive candidates; the XML rule does not apply to Python >=3.14, and the language cookie is not sensitive. Service-worker message validation needs its separately tracked assessment. No blanket suppressions or dependency changes were added.
  • Live browser verification was unavailable because browser access to localhost was denied. Node checks execute the actual calculation, conversion and rendering functions; Flask checks exercise real race routes.

Implementation decisions

  1. Use the existing feature checkout authorised by the user. Cost: no additional worktree isolation.
  2. Preserve legacy estimated_gap semantics and add explicit fields. Cost: older API consumers retain the legacy anchor's naming; the new UI never calls it grade-only GAP.
  3. Use ES modules and the existing cache-busting import map instead of the plan's CommonJS option. Cost: the browser requires that import map; Node tests resolve the same module sources.
  4. Update the old label tests to the approved two-result design. Cost: the previous GAP-input wording contract is intentionally superseded; the distinction guard remains.
  5. Leave live forecast accuracy as an external-condition caveat. Stable forecasts verify the solve; newer forecasts can change totals.
  6. Accept independently run local Docker and Snyk evidence for the reviewer's unassessed environment checks. Cost: the pre-existing security debt remains tracked separately.
  7. Preserve the unrelated untracked test_pacing_mode_parity.py. Cost: its separate parity assertions are not evaluated by this task.