Pitwall Backend Audit — 2026-04-28¶
Historical snapshot. File paths below reflect the layout as of 2026-04-28. Since then PR #30 reorganised
src/pitwall/features/coaching/(splitcoach_engine.pyinto 6 modules behind a back-compat shim) and promoted helpers from the now-deletedhelpers.pyintofeatures/session/laps.py,features/session/frames.py,features/coaching/cue_renderer.py, andfeatures/track/track_json.py. Seeinternal_architecture.mdfor the current module map. Note: pitwall_bridge.py was deleted in a prior commit; bridge entry is now src/pitwall/main.py.
Audit of all Python code shipped in this work cycle (src/pitwall/features/, tools/). Catalogs:
- Bugs — incorrect behaviour observed or trivially provable.
- Weak spots — code that works but has known fragility (data dependence, edge cases).
- Architectural risks — decisions that may need revisiting based on field-test feedback.
Each item is tagged [FIX] if it gets patched in this turn or [DEFER] if it's a known limitation we're shipping with.
src/pitwall/features/sonoma.py¶
- ✅ Constants module is straightforward;
LAP_TIME_LEVERAGEweights validated to sum to 1.0 at import time. - [FIX]
WeatherPhase.start_hour=18, end_hour=18is missed at hour 18+ —weather_phase_for_hour(20)should returnlate_session. The current loop with<end_hour misses the boundary. Patch the loop to use<=or extend the last phase. - [FIX] No test asserts
LAP_TIME_LEVERAGEcovers exactly the 11 entries ofCORNER_ORDER. Drift between the two would silently miscompute weighted scores.
src/pitwall/features/track_loader.py¶
- ✅
MarkerDefdataclass +find_nearest_marker+find_marker_for_next_cornerwork correctly on the canonical sonoma.json. - [FIX]
find_nearest_markerwithkind=Noneandcorner=Noneand a marker exactly attrack_distreturns None because the gap-check is0 < gap < lookahead. A marker exactly at the car's position should be the answer; current code skips it. Change to0 <= gap < lookaheadand let the cooldown logic deduplicate. - [DEFER]
cross_track_errordoes point-to-point haversine to the nearest reference_line vertex, not perpendicular distance to the polyline segment. Approximate but introduces ~1 m error near corners with sparse reference points. Acceptable for current use; noted for future.
src/pitwall/features/vbo_parser.py¶
- ✅ Now extracts the
avitimefield for video sync.TelemetryFrame.avitimedefaults to 0 when absent. - [DEFER] Per-frame
import mathinside the parsing loop — minor inefficiency, doesn't justify a refactor. - [DEFER] No unit conversion check on
avitime— assumes milliseconds (correct for Racelogic VBO format but brittle if a different vendor's file is used).
src/pitwall/features/gold_standard.py¶
- [KNOWN — DEFER] Lap detection still imperfect with the dataset's distance counter. The fastest detected single lap is usually a partial slice rather than a full lap when the file contains many runs. Per-corner aggregation (the more important output) is robust; the headline
lap_time_sis the weak number. - ✅ The "best pass per corner" picker now scores by
(min_speed_kmh, exit_speed_kmh)with a slowed-at-apex sanity filter, notcorner_time_salone — fixes the earlier "blew through the corner at 170 km/h" bug. - [FIX]
_aggregate_corner_pass_absreturns 0 forbrake_point_mandbrake_release_mwhen the driver never brakes for that corner. Should return None ornanso the grader can distinguish "no data" from "brake at entry". Patch the dataclass + grader to handle None.
src/pitwall/features/corner_grader.py¶
- ✅ Weighted A-F formula matches
feedback-system.md:118-140spec. - ✅ Time-loss decomposition has 8 cause categories with capped attribution to prevent over-counting.
- [FIX]
_trod_voice_forreturns "Nice — keep that line." for any corner with empty attribution, including high-leverage corners where it should suggest something more nuanced. Add a corner-specific positive voice line for graded-A passes. - [DEFER] Time-loss attribution coefficients (e.g.
0.025 * apex_deltafor low_apex_speed) are heuristic, not calibrated. Calibration requires multiple gold-vs-driver lap pairings — Phase 4 work.
src/pitwall/features/analytics.py¶
- ⚠ [FIX] Critical bug in
smoothness_per_corner: the_track_corners_cachehelper is a stub that returns inert tuples(name, 0, track_len)for every corner. As a result, the function reports the SAME corner span (the entire track) for every corner — meaning all "in-corner" frames are everything, the std-devs are computed over the whole lap, not per-corner. The output is a dict keyed by corner name but the values are session-wide, not corner-specific. Patch needed: refactorsmoothness_per_cornerto take thetrackobject (already loaded by the caller) and use real corner entry/exit distances. - ✅
friction_circle,hustle_map,consistency,eob_summary,slip_angle_band,change_in_speed_events,trail_brake_events,flight_recorder,limit_oscillation,plateau_detectorare all correct against synthetic inputs. - [FIX]
friction_circlesamplesfield caps at 1500 entries with no downsampling — a 8000-frame lap's first 1500 frames don't represent the whole session well. Use stride sampling (frames[::n]so all parts of the lap are represented).
src/pitwall/features/highlight_finder.py¶
- ✅ All 7 Sonoma-specific categories detect from synthetic frames as expected.
- [FIX] "Perfect trail brake at T4" detector: the condition
0.5 <= (apex/peak) <= 0.20is malformed —0.5 <= x <= 0.20is always False. Should be0.05 <= ... <= 0.20. The detector currently never fires. - [DEFER] Severity ordering puts "engineering" lowest (correct) but "positive" sits between "medium" and "engineering". For the post-session debrief we may want positives before engineering events. Acceptable default.
src/pitwall/features/driver_profile.py¶
- ✅ Schema is append-only, idempotent.
compute_profilecorrectly identifies weakest_recent_corner and biggest_improvement. - [FIX]
compute_profilebiggest_improvementcompareshs[0] - hs[1]after ordering DESC byrecorded_at. If the most-recent score is the worst, it returns a negative delta (regression, not improvement). Filter todelta > 0before picking the biggest.
src/pitwall/features/session_analyzer.py¶
- ✅
_segment_into_lapscorrectly assigns lap indices via cumulative-distance progression. - [FIX] Templated narrative writes
Δ: +-63.00 swhen the best lap is shorter than the gold (negative delta). The format string+{delta:.2f}produces+-63.00instead of-63.00. Use{delta:+.2f}consistently OR special-case the sign. - [FIX] When
_no_gold_bundletriggers,consistencyandeob_summaryare empty dicts butfrictionandslip_bandstill get computed — inconsistent behaviour. Either compute all-of-them or none-of-them.
src/pitwall/features/coach_engine.py¶
- ✅
RuleCoach,LitertCoach,CoachContext,CoachArbiter, all three system-prompt builders work correctly with synthetic inputs. - ✅
LitertCoachfalls back toRuleCoachwhen MediaPipe is missing — verified by smoke test. - ✅
make_coach("auto")prioritylitert > ruleworks. - [DEFER] No way to inject a custom system prompt at runtime.
build_system_promptis pure (deterministic) but doesn't accept overrides for testing variant prompts. Could be added without breaking the contract.
src/pitwall/main.py¶
- ✅ 26 endpoints, all importable cleanly. Schema initialises on first use.
- [FIX]
_section()returns aResponsewith implicit 200 when bundle is missing. Should return(jsonify(...), 404)consistently — already done insession_detailandsession_clips, missing in_sectionitself. Fix returns to be(response, status)tuples. - [FIX]
_load_session_framesis called inside the request handler with the DB lock held briefly, then releases. If multiple debriefs hit the same session simultaneously the analyser runs twice. Add an in-memory lock per session_id around_session_bundleswrites. - [DEFER] The
/coach/briefdriver_id="" (empty string) case computes profile against an empty driver — which returns no profile data, which is fine, but also writes empty events on the post-debrief side ifpersist_to_profile=Trueanddriver_id="". Should skip persistence when no driver_id. - [FIX]
session_syncSQL parameter bindings are constructed in two passes (paramsthenparams2) — the first list is unused; cleanup.
tools/enrich_sonoma_track.py¶
- ✅ Idempotent. Writes
.bak. Syncs to all 4 duplicate copies. - [FIX]
_sample_reference_linereturns(0.0, 0.0)if the reference_line is empty — better to returnNoneso callers can detect missing data instead of pinning the marker at the equator.
tools/extract_gold_lap.py¶
- ✅ Default paths work. Output JSON is round-trippable.
- [DEFER]
--vbodefaults to a hardcoded forza path. Acceptable — this is a dev tool, not a production deploy.
tools/best_sonoma_lap.py¶
- ✅ S/F-line projection + sign-change detection works (22 sessions found vs 0 with the old radius-only detector).
- [KNOWN — DEFER] Best lap times are ~2:28 vs the filename's claim of 1:47.5. The dataset's anonymised GPS may scale the track differently. Can't fix without ground-truth lap data; the relative ranking is still correct.
tools/import_sonoma_real_gps.py¶
- ✅ Stitches OSM Overpass results into a closed loop (88 m closure gap, ~1% scale match against anonymised).
- [FIX]
map_fractionalis invoked twice per marker (once for lat, once for lon). Cache the call. - [DEFER] No retry/timeout handling for the Overpass request. Acceptable for a one-shot tool.
tools/extract_marker_thumbnails.py¶
- ✅ Produces correct cut points (verified: 16 markers, average 0.3 m frame gap). Falls back to dry-run when ffmpeg is missing.
- [DEFER]
find_frame_at_distancedoes a full O(n) scan for each marker — 16 × 8000 frames = 128K comparisons, milliseconds. Not worth pre-indexing.
tools/validate_litert.py¶
- ✅ Self-contained, lazy imports, runs without MediaPipe installed (it'll FAIL at step 1 cleanly).
- [DEFER] No way to test this from a non-Pixel environment without mocking MediaPipe — that's by design.
Summary of fixes landing this turn¶
| File | Fix |
|---|---|
sonoma.py |
weather_phase_for_hour boundary at hour 18 |
track_loader.py |
find_nearest_marker includes gap=0 |
gold_standard.py |
brake_point_m / brake_release_m use None when no brake event |
corner_grader.py |
per-corner positive voice line for graded-A passes |
analytics.py |
smoothness_per_corner rewritten to use real track corners |
analytics.py |
friction_circle stride-samples instead of head-of-lap |
highlight_finder.py |
T4 trail-brake condition fix (0.05 <= … <= 0.20) |
driver_profile.py |
biggest_improvement filters to positive deltas |
session_analyzer.py |
sign formatting in templated narrative |
pitwall_bridge.py |
_section() returns 404 with status code |
pitwall_bridge.py |
session_sync parameter cleanup |
enrich_sonoma_track.py |
_sample_reference_line returns None on empty |
import_sonoma_real_gps.py |
cache map_fractional call |
Everything else is [DEFER] — known but acceptable.
Test plan¶
Test files in a new tests/ directory at the repo root, runnable with pytest tests/. Each module gets a focused unit-test file plus an integration test that exercises the full session → debrief flow. See tests/conftest.py for shared fixtures.