Alignment review page generated 2026-07-04 for the experimental Pitwall Track Pace popover.
Summary
Audit verdict: The track geometry and car/shadow placement math are internally coherent and covered by focused tests. Actual pace is mapped as usage percent to lap fraction; the shadow car is mapped as ideal target percent to lap fraction; animated speed is driven by the clamped actual-vs-target ratio.
Main concern: Daily scope intentionally uses a whole-day target for the track and a session-aware, sometimes rebalanced target for timing bars. That is defensible, but it needs explicit product acceptance and probably one more integration-style test to guard against future confusion.
Likely edge risk: The track clamps target percent to a lap fraction. When a daily target becomes zero or overdrawn, the shadow sits on the start line or disappears as a meaningful target while the status can still be critical. The daily segment UI has richer overdrawn copy, but the track surface does not show that nuance.
Scope
This audit reviewed how Pitwall calculates and renders the new race track representation, with emphasis on:
- Track path representation and mapping from progress to circuit position.
- Daily/session/weekly percent and target calculation used by Track Pace.
- Actual car placement, shadow car placement, and animation speed.
- Daily timing tower relationship to the whole-day track.
Primary files reviewed: apps/pitwall-local/Sources/PitwallAppSupport/TrackPaceExperiment.swift, apps/pitwall-local/Sources/PitwallApp/Views/TrackPaceSceneView.swift, apps/pitwall-local/Sources/PitwallAppSupport/ProviderUsageRowsViewModel.swift, and apps/pitwall-local/Sources/PitwallAppSupport/DailySessionPaceCalculator.swift.
Findings
PassTrack geometry is represented as an arc-length parameterized closed loop.
The circuit model stores dense normalized waypoints for 12 tracks and builds TrackPath(denseLoop:). position(atProgress:) converts fractional lap progress to total arc-length progress with binary search over cumulative lengths, so 50% progress means halfway around the path by distance, not halfway through waypoint index count.
Evidence: TrackCircuit.waypoints and TrackCircuit.path() at lines 103-137; TrackPath.position(atProgress:) at lines 194-217. Tests cover start line, full-lap wrap, half-lap distance, distinct quarter positions, direction, unit box normalization, dense GPS trace use, and aspect preservation.
PassActual car and shadow car positions are mathematically consistent at data refresh.
TrackPaceStanding.lapFraction clamps percent to 0...1. The actual car uses usagePercent / 100; the shadow uses targetPercent / 100 when a target exists. At elapsed zero, TrackPaceLapMotion returns those exact anchors.
Evidence: usageLapFraction, targetLapFraction, and lapFraction at lines 360-371; static and animated progress accessors at lines 420-438. Tests assert 42% usage and 60% target place the car and shadow at the corresponding circuit fractions for every circuit.
PassAnimation speed communicates burn-rate direction without changing the refresh-time truth.
The shadow laps at a fixed 14-second reference duration. The actual car speed uses usagePercent / targetPercent, clamped to 0.25...2.0. A behind-pace car moves slower than the shadow; an over-pace car moves faster. At each data refresh, both cars re-anchor to the real actual and target fractions.
Evidence: TrackPaceLapMotion ratio and progress math at lines 381-438; SceneKit render loop at TrackPaceSceneView.swift lines 193-232. Tests cover drift direction, ratio clamps, wrap across the line, parked capped car behavior, and static re-anchoring after animation.
WatchDaily track target and timing tower targets are intentionally different surfaces.
The daily track uses the whole-day usage/target pair, while the Daily row signal can be burst-aware through current session segments. The code explicitly changes daily track signal to follow the whole-day pair so the label never contradicts the car-shadow gap, then surfaces burst risk through currentPaceRunningHot and timing rows.
This is coherent, but it is easy to misread because the timing tower's segment targets can be session-aware and rebalanced after observed usage. Product copy should make clear that the track is day-level and the tower is session-level.
Evidence: design comment at TrackPaceExperiment.swift lines 9-14; daily builder branch at lines 621-643; dailyTrackSignal at lines 651-671; daily row segment enrichment at ProviderUsageRowsViewModel.swift lines 777-815.
RiskZero or overdrawn daily target weakens shadow-car semantics.
weeklyFinishDailyTarget can return 0 when the day is after or at reset boundary, and segment rebalance can create negative target percent when previous/current observed usage has already consumed more than the daily budget. Segment copy handles negative targets as overdrawn, but TrackPaceStanding.targetLapFraction clamps any negative target to 0. The shadow at the start line no longer means "ideal progress" in a user-obvious way.
This is probably acceptable for an experiment, but it should either be called out in UI copy or tested and deliberately accepted. A possible improvement is to hide or relabel the shadow when the target is <= 0, while keeping the status as critical/limit-hit.
Evidence: weeklyFinishDailyTarget at lines 1061-1103; rebalance remaining budget math at DailySessionPaceCalculator.swift lines 355-394; target fraction clamp at TrackPaceExperiment.swift lines 365-371.
Coverage GapNo provider-level Track Pace test proves daily target parity with paceTargetPercent under history/local pacing inputs.
The focused tests are strong around geometry, animation, and simplified provider state. The narrower missing test is an integration-style case where daily row.percent, paceTargetPercent("Daily"), dailyTrackSignal, and daily segment/tower data are all asserted from the same provider fixture with history snapshots and local pacing records. That would lock down the intended whole-day-versus-session split.
Race Track Representation
The representation is data-driven rather than hand-drawn per circuit at render time. Each TrackCircuit exposes generated normalized waypoints, race direction, display name, and flag. TrackPath preserves the dense loop as samples and calculates cumulative length once. Rendering uses a ribbon generated from the centerline and adaptive width.
Scene framing is also nontrivial: the camera fit projects every path sample into normalized FOV bounds, binary-searches distance, then pans the target to center the circuit. Tests check every circuit remains visible, centered, and large enough in the assumed view aspect.
Actual Pace and Shadow Car
The current implementation's semantics are:
- Colored car: actual utilization progress for the selected scope.
- Shadow car: ideal target utilization progress for the selected scope, hidden when no target exists.
- Gap: actual minus target in utilization points, classified by the same 6/12 point bands as whole-window pace signal.
- Animation: shadow at reference speed; actual car speed scaled by actual/target ratio; capped usage parks the car at the line.
For daily scope, "actual pace" means day-to-date usage, not current session burn. The current-session burn can make the main row critical while the track stays calm. The code intentionally shows a running-hot hint in that case. This is not a math bug, but it is a product semantics decision.
Verification
Focused test command run from the repo root:
swift test --package-path apps/pitwall-local --filter 'TrackPaceExperimentTests|TrackPaceSceneTests|TrackPaceTimingRowTests|DailySessionPaceCalculatorTests'
Result: passed. The command executed 54 selected tests with 0 failures in the XCTest suites. The selected coverage included 34 TrackPaceExperimentTests, 5 TrackPaceSceneTests, 6 TrackPaceTimingRowTests, and 9 DailySessionPaceCalculatorTests.