@inventure71/paddockjs 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/grid_walk.md +84 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +38 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +24 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +106 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
package/docs/rules.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Race Rules
2
2
 
3
+ ## Optional Grid Walk presentation
4
+
5
+ [Grid Walk](grid_walk.md) holds normal browser playback before the existing start sequence. The first Race transition clears the crew before simulation time advances; reduced/paused motion clears immediately. Returning later pauses the race and renders original starting poses without writing them into physics. Resuming preserves time, laps, RNG, cars, tires, pits and order. Rolling starts remain rolling starts. Only explicit restart creates a fresh race.
6
+
3
7
  This file documents the rules currently implemented by the simulator. If the race engine changes, update this file with the behavior change.
4
8
 
5
9
  ## Default Race Rules
@@ -190,6 +194,8 @@ The simulation builds hidden timing lines around every track at an F1-style mini
190
194
 
191
195
  Seconds gaps are the difference between when two cars crossed the same timing line. The race engine falls back to timing-history interpolation only before the cars have a shared timing-line sample. The timing tower can switch at runtime between `Int` mode, which displays interval to the car ahead, and `Gap` mode, which displays direct gap to the leader. When the relevant gap is one or more whole laps, the tower shows `+1`, `+2`, and so on instead of a misleading seconds estimate. Timing continues to be calculated during pre-start, safety-car, and post-finish states even when the UI shows state labels such as `Grid`, `SC`, or `FIN`.
192
196
 
197
+ Lap telemetry clamps `completedLaps` to the configured `totalLaps`, including overshoot in large or accelerated fixed steps, so post-finish snapshots cannot report more completed laps than the race distance contract allows.
198
+
193
199
  ## Units
194
200
 
195
201
  The race engine uses simulator units internally. `src/simulation/units.js` converts simulator distance and speed to public meter and km/h values. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds. Public distance values with a `Meters` suffix are meters. The current speed calibration maps the simulation maximum speed to an F1-like `330 km/h`; rendered car sprite dimensions are a visual scale and are not used as the physical distance scale.
@@ -198,7 +204,23 @@ The race engine uses simulator units internally. `src/simulation/units.js` conve
198
204
 
199
205
  The default `physicsMode` is `'arcade'`, which preserves the existing forgiving vehicle behavior for current browser hosts and demos.
200
206
 
201
- `physicsMode: 'advanced'` is opt-in for more believable F1-style limits. It still accepts only normal steering, throttle, brake, and pit-intent inputs, but the vehicle integrator keeps a real 2D world velocity, derives speed from that velocity, and applies a traction budget between longitudinal and lateral demand. Steering changes yaw through a rate-limited response, while body slip is derived from the difference between the car heading and actual velocity direction instead of being an artificial movement offset. External state placement through simulator APIs rejects non-finite numeric values and resynchronizes `velocityX/Y` whenever speed or heading is changed without an explicit velocity vector, so scenario/reset placement cannot leave stale advanced-mode velocity behind. Kerbs remain legal racing surface, but high-load kerb use is less stable and slower than track asphalt. Gravel and grass are averaged per wheel so one wheel over the edge creates partial grip/drag loss, while all-wheel runoff still costs major time. The rendered barrier wall is the hard outer runoff boundary in both physics modes; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision and sensor participation, and placed in DNF timing order. Barrier destruction, stalled DNF, pre-start grid hold, red-flag hold, and pit-queue hold freeze scalar speed and advanced-mode velocity together; clearing a red flag resynchronizes active cars to the release speed and heading before the next observation while DNF/out-of-race cars remain frozen. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Advanced mode does not teleport cars, magnetize them to the track, or bypass physics with hidden pose correction.
207
+ `physicsMode: 'advanced'` is an opt-in planar Formula-style handling model. Four contact patches produce longitudinal/lateral forces from local wheel velocity, surface grip and load. A nonlinear lateral brush curve shares each tire's friction circle with rear-wheel drive or braking. Centered 50/50 static mass, longitudinal and lateral load transfer, tire load sensitivity, front brake bias, aerodynamic downforce and drag affect the available forces. Force and moment sums integrate world velocity and yaw inertia at internal steps no larger than 1/240 s. Steering commands set a rate-limited physical wheel angle; speed-dependent keyboard conditioning belongs to the human input layer, not vehicle physics or policy actions. Tire forces create steering resistance through their directions; the old synthetic steering-scrub and target-yaw formulas are removed.
208
+
209
+ The demo advanced lab applies human-only keyboard assistance: key travel ramps before speed-dependent steering lock, and observed tire overload reduces the digital accelerator demand to reserve turning grip. It does not steer toward the track or change the physical forces. Its host scheduler caps stepping at 60 Hz independently of display refresh and discards catch-up debt after a stall. External policy action scaling and the general controller-loop scheduler contract are unchanged.
210
+
211
+ The existing setup ratings remain intact. For the baseline car, `powerNewtons` times 0.5 sets peak rear drive force, transitioning at 28 m/s to constant power (602 kW for the default 43000 N rating). `brakeNewtons` is apportioned 57% front / 43% rear before tire adhesion; full default braking can hold the car against full throttle. Braking and rolling resistance share a contact stopping-impulse budget so they cannot create reverse motion or energy at rest. DRS reduces aerodynamic drag and downforce. The existing 330 km/h integration envelope is retained. These are explicit simulation calibrations, not proprietary F1 data.
212
+
213
+ Body slip is actual velocity direction minus body heading. Kerbs are legal but reduce available grip; a slipping car can lose steering authority rather than always losing more scalar speed. Gravel and grass affect each contact patch separately, including asymmetric resistance and yaw moments. Wheel load transfer is quasi-static: no suspension travel, vertical motion, tire carcass/temperature model or individual wheel rotational state is simulated. Brake demand beyond adhesion is approximated as a contact sliding against its actual velocity; gear shifts, wheel-spin dynamics and a full collision manifold are outside this model.
214
+
215
+ Advanced car contacts use mass/inertia-dependent normal impulses with a contact point approximated from opposing oriented body support features. They do not directly rotate the heading. Arcade contact response is unchanged. Existing fixed pit-car ownership, contact event deduplication and stewarding remain authoritative. Rendered barriers still destroy cars at the same footprint boundary; advanced physics does not soften that race rule.
216
+
217
+ External state placement rejects non-finite values and resynchronizes world velocity when speed/heading changes without an explicit velocity vector. Grid/red-flag/queue/terminal holds freeze velocity, yaw and acceleration together. Pit routing remains the existing bounded route movement and service model. Advanced racing AI follows the velocity direction and adds the front tire slip needed to produce its requested turning force. Its race-specific speed planner uses dynamically transferred contact loads and the weaker yaw-balanced axle rather than a fixed share of static total grip. It solves upcoming corner limits using downforce at the candidate corner speed, then works backward through a sampled braking envelope that reserves lateral grip. The actual overtaking/correction path can impose a tighter limit than the centerline. A bounded force controller compensates drag and rolling resistance and responds quickly enough to follow that braking envelope. The 330 km/h package envelope is a ceiling, not a guaranteed speed: engine force, drag, tires, traffic and track geometry determine the achieved pace.
218
+
219
+ The advanced racing controller matches a slower car's speed and physical following gap until there is actual lateral clearance, even when a pass is planned. It retains the existing lane selection, traffic penalties and recovery/safety-car controllers. All changes produce ordinary steering/throttle/brake commands; there are no pose writes or extra grip. Pace and racecraft determine control ability; consistency determines bounded continuous line and speed-judgement variation. Risk tolerance and aggression independently reduce the chosen margins. Skill does not add physical grip or power, and mistakes are not scheduled accidents. Advanced corner force budgets range from 74% to 96% of the predicted dynamically loaded balanced-axle bound; longitudinal budgets range from 86% to 98%. These policy budgets are not measured utilization of every tire. The current vehicle, tires, surfaces, DRS and traffic remain limiting.
220
+
221
+ Advanced braking preview inspects contiguous geometry from the current position. It reserves the steering pursuit distance, the controller yaw-response and driver anticipation distances, and one sampling interval before counting available braking distance. It must not shift its sampling origin past nearby bends. Pedals reserve lateral adhesion and account for rear-drive allocation, brake bias, load transfer and resistance. The bounded brake solver handles a feasible nonzero braking window even when zero braking overloads the front axle. Arcade racing uses its own actual yaw and longitudinal limits, with a shorter smooth pursuit horizon in tight geometry; it does not introduce an advanced friction circle. Recovery, safety-car and automatic pit controllers retain their separate ownership.
222
+
223
+ Measured pace, incidents, rejected alternatives and limitations are recorded in [the first advanced AI pace report](advanced_ai_pace.md), [the both-mode racing AI report](racing_ai_improvements.md), and [the skill-dependent limit-driving report](limit_driving_2026-10-14.md).
202
224
 
203
225
  Simulator snapshots and environment observations expose additional vehicle telemetry: `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. These fields are optional public telemetry; existing `speedKph`, wheel-surface, steering, throttle, and brake fields remain unchanged.
204
226
 
@@ -206,11 +228,13 @@ Simulator snapshots and environment observations expose additional vehicle telem
206
228
 
207
229
  Lap is computed from each car's cumulative race distance over the track length. Total laps are provided by mount options and default to `10`.
208
230
 
231
+ Hidden timing-line crossings use exact forward `(previousDistance, currentDistance]` boundaries and the same representable line distance for indexing and recording. A car infinitesimally before a line must still record that crossing; no tolerance band may silently drop a lap timestamp.
232
+
209
233
  Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Completed current-lap sector split times before the active sector are exposed as `currentSectors`, while `liveSectors` keeps only the active sector's elapsed time updated before the boundary is crossed and clears any future-sector values. `sectorProgress` is recomputed every snapshot as the live S1/S2/S3 fill state for the current lap, so sector-map UI does not derive progress from completed split state or keep stale future sectors lit. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
210
234
 
211
235
  Each car is marked `finished` when it reaches `track.length * totalLaps`. It receives a frozen provisional `finishRank`, exposes `raceStatus: 'waved-flag'` / `wavedFlag: true`, and emits a `car-finish` event. The first finisher becomes provisional `winner`, but the race keeps running until every race participant has either crossed the finish distance or is currently DNF. While the field is still finishing, timing order keeps already-finished cars in provisional finish order ahead of cars still running, and DNF cars sit below active cars in the order they retired. A car that has already crossed the finish line keeps that result even if it later touches the barrier or satisfies stalled-off-track detection while the rest of the field is still finishing; post-finish retirement checks do not turn it into DNF. A DNF car restored before final classification re-enters the live order and must finish before the race can complete; restoring it after final classification does not reopen the race.
212
236
 
213
- When all cars have finished or reached DNF, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result after outstanding penalties and classification consequences, deploys the safety car, and switches race mode to `safety-car`. DNF classification entries appear after finishers with `finished: false`, no finish time, and DNF reason/time/order metadata. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement championship scoring.
237
+ When all cars have finished or reached DNF, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result after outstanding penalties and classification consequences, deploys the safety car, and switches race mode to `safety-car`. DNF classification entries appear after finishers with `finished: false`, no finish time, and DNF reason/time/order metadata. Finished cars keep circulating under safety-car behavior instead of hard-stopping. Timing presentation derives its final state from `raceControl.finished`, so winner and `FIN` labels remain final even though circulation mode is now `safety-car`. The current implementation does not yet implement championship scoring.
214
238
 
215
239
  ## DRS
216
240
 
@@ -263,7 +287,7 @@ The AI has separate behavior for:
263
287
  - Off-track rejoin.
264
288
  - Safety-car queueing.
265
289
 
266
- Green-flag behavior uses:
290
+ Arcade green-flag behavior uses:
267
291
 
268
292
  - Driver aggression.
269
293
  - Racecraft.
@@ -271,22 +295,22 @@ Green-flag behavior uses:
271
295
  - Patience.
272
296
  - Current tire energy.
273
297
  - Track curvature.
274
- - Capped preview curvature for braking decisions.
275
- - Forward track-heading feed-forward for corner entry.
298
+ - Distance to upcoming bends and available braking distance.
299
+ - Geometric pursuit of a forward lane target, adjusted for tire response.
276
300
  - Nearby traffic.
277
301
  - Preferred and available lane offsets.
278
302
 
279
- The built-in racing controller still drives through normal vehicle inputs only. It chooses a forward racing target, blends steering from the target angle, upcoming track heading, lateral offset, edge recovery pressure, outward edge motion, and a corner-apex racing-line offset, then sends clamped steering/throttle/brake values into the vehicle physics integrator. It is expected to use the available track width and kerbs on generated circuits, including sharp generated turns. When heading alignment, steering load, or outward edge motion shows the car cannot yet carry throttle through a corner, throttle is cut before braking is requested so the controller does not fight itself. It does not snap car position, override heading, or bypass steering-rate/yaw-rate limits.
303
+ The arcade racing controller drives through normal vehicle inputs only. It chooses a forward racing target using lane intentions, corner offsets and edge pressure, then inverts the arcade bicycle yaw response to steer toward that target. This avoids contradictory target-angle and future-heading steering commands. The driver samples upcoming corners and retains speed until braking distance requires a slowdown, reserving reaction distance and conservative deceleration. Driver ratings and DRS influence desired pace, but a tire-condition-aware yaw-capacity ceiling takes precedence over corner and maneuvering speed floors. Heading/edge/steering-load penalties and bounded pedal controls remain active. A backward-facing car enters the existing recovery controller before forward racing pursuit resumes. No position, heading, force or steering-rate limit is overridden.
280
304
 
281
- In advanced mode, the built-in controller uses separate tuning: earlier lookahead for braking, smaller high-speed steering requests, stronger edge recovery before the legal limit, throttle reduction when grip and slip telemetry show the car is overloaded, a higher legal-road speed floor so the baseline AI does not crawl through generated bends, and lower rejoin targets from kerb, grass, or gravel states. Extra stability braking is applied only when upcoming corner load, edge pressure, outward drift, high straight speed, or near-target speed makes it useful; ordinary below-target straight-line correction should not trigger brake pulses. This is intentionally a baseline driver, not a trained policy.
305
+ Advanced green-flag driving uses velocity-oriented pursuit with inverse tire-slip steering, a candidate-speed corner/braking envelope and bounded force control described under [Physics Modes](#physics-modes). The additional following limit uses actual chassis-center spacing and the speed of a physically blocking car; lane selection alone is not evidence that a pass has cleared it. While cornering consumes the racing grip budget, the driver reduces lateral movement toward the planned lane and holds a consistent line. It progressively releases the lane change as corner load falls on exit. Driver ability and risk determine the chosen lateral and longitudinal margins within the four-contact physical envelope; braking reserves lateral demand and the pursuit/response/anticipation distance. The former fixed 60% corner and 55% braking shares are superseded by this policy. The built-in driver is a deterministic heuristic policy, not a trained model or a globally optimal racing-line solver.
282
306
 
283
- Traffic decisions use real meter-scaled gaps and lateral spacing, not raw simulator-size constants. A trailing car inside the attack window commits toward a visible passing lane instead of sitting on the centerline. A leading car with a close rear threat can defend the threatened side before the rival is fully alongside. Side-by-side overlap still creates spacing pressure so defensive and attacking moves are expressed as target-line choices and normal steering/brake/throttle inputs, not as collision avoidance teleports or artificial position overrides.
307
+ Traffic decisions use wrapped physical track gaps and chassis-center lateral spacing in the retained main-track frame. A lapped car or a car across the start line can be the nearest physical passing/following target regardless of classification rank. Retired, destroyed and collision-disabled participants do not create blocking or defensive pressure; separate pit-lane traffic is excluded from tactical lane targets while overlapping pit-entry traffic still counts. Race classification and timing gaps keep their existing cumulative-distance meaning. Current measured comparisons and limitations are recorded in [the v11 improvement evidence](v11_improvements.md).
284
308
 
285
- Rejoin behavior also remains physics-driven. When a car leaves the legal racing surface, the controller enters a short recovery hold and keeps using rejoin controls after the center point first reaches track or kerb until heading and edge margin are stable. That prevents the car from immediately returning to full racing throttle while still pointed across the track. Recovery targets look forward along the racing surface, bias inward from the edge, and request bounded surface-specific speeds for track, kerb, grass, and gravel states. In advanced mode, rejoin controls also watch signed outward velocity relative to the track normal: a car sliding farther into runoff cuts throttle or brakes before trying to accelerate out, while very low-speed gravel/grass recovery limits steering lock and caps throttle when the heading is still outward so steering scrub does not pin the car in place. Barrier contact is not a recovery state; it destroys the car.
309
+ Both modes match a physically blocking car's speed and following gap until lateral clearance exists. This limit takes precedence over arcade maneuvering speed floors, so stopped traffic can require stopping. A committed pass suppresses conflicting rear-defense movement, and a driver does not defend against a physically rearward car that is lapping it. Arcade drivers hold their current chassis line during loaded turns with nearby traffic and release that hold on exit; inward edge recovery takes priority. Advanced drivers constrain their final target to the lateral corridor available beside overlapping cars, rather than checking only an intermediate lane intention. Their corner line hold also permits inward correction near the road edge. These choices remain ordinary steering/brake/throttle inputs with no physics overrides.
286
310
 
287
- Advanced-mode steering scrub is dissipative in both forward and reverse motion. It opposes the signed forward velocity instead of always applying negative forward acceleration, so a car sliding backward with zero throttle cannot gain speed from steering scrub.
311
+ Rejoin behavior remains physics-driven. A recovery hold prevents a car from immediately returning to racing pace while heading or edge margin is unstable. Arcade recovery uses inward-biased targets and outward-edge-motion checks. Advanced recovery follows a centerline target ahead at reduced surface-dependent speed, budgets lateral tire capacity and arrests a large sideways/backward slide before accelerating toward the track. At crawling speed, worn-tire runoff recovery includes rolling resistance within the rear tires' available adhesion so the car is not held below the force needed to move. Barrier contact still destroys the car; recovery does not soften that rule.
288
312
 
289
- Tire energy can degrade to 1%. The vehicle physics layer converts tire energy into a nonlinear grip factor, so degradation has a visible performance cost across the full 100% to 1% range while still leaving a damaged car controllable enough to return to the pits.
313
+ Tire energy can degrade to 1%. The vehicle physics layer converts tire energy into a nonlinear grip factor, so degradation has a visible performance cost across the full 100% to 1% range. Recovery is not guaranteed on every surface: in arcade mode, sufficiently worn tires at rest in gravel can have less available drive acceleration than rolling resistance. The racing AI accounts for remaining corner capacity to avoid unnecessary departures; it does not add recovery force.
290
314
 
291
315
  ## Vehicle Physics
292
316
 
@@ -323,11 +347,13 @@ Track state can classify a car as on:
323
347
 
324
348
  Surface affects grip, drag, and rolling resistance.
325
349
 
326
- Surface physics is wheel-based. `src/simulation/vehicle/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, and pit connectors first resolve wheel centers through local connector geometry before falling back to full patch sampling only for genuinely ambiguous pit-road overlap. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. If the car footprint reaches the rendered barrier wall's inner face, every physics mode marks the car destroyed instead of treating the wall as recoverable low-grip road. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces and do not count toward stalled off-track DNF timing.
350
+ `src/simulation/vehicle/wheelSurface.js` classifies each wheel contact patch against the shared track model. Its worst-surface classification (`barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`) remains the car's reported surface and arcade grip/drag input. Arcade also retains its capped asymmetric wheel-resistance yaw bias. Advanced physics instead uses each wheel's own surface and normal load to calculate forces; one wheel on gravel is not equivalent to four wheels on gravel.
351
+
352
+ Normal main-track wheel queries use the existing analytic signed-offset path; pit connectors resolve wheel centers locally and use full patch sampling for ambiguous overlap. One wheel on a kerb does not itself violate track limits. Reaching the rendered barrier wall's inner face destroys the car in either mode. Pit entry, lane, exit and box remain legal surfaces and do not count toward stalled off-track DNF timing.
327
353
 
328
354
  Rendered material bands use those same thresholds. The visible gravel band starts at the outer kerb edge and ends at the simulated gravel edge; the visible runoff band starts at the gravel edge and ends at the barrier centerline; the barrier wall is drawn from the same `track.barrierWidth` used by destruction. Rendering, ray surface hits, wheel-surface classification, and barrier destruction must stay on this shared geometry contract.
329
355
 
330
- When pit stops are enabled, each team is assigned one shared service area in the working lane plus two garage boxes. Driver pairs share the service area, and the service area, queue slot, and garage boxes inherit the team color from `driver.team.color` when present, otherwise the lead driver's color. The current automatic stop plan distributes pit calls across available race laps as bounded pit trains, then lets opportunistic cars join the entry only when fewer than `pitStops.maxConcurrentPitLaneCars` are active and the nearest active pit-lane car is at least `pitStops.minimumPitLaneGapMeters` ahead; committed cars enter at the next pit-entry window and can queue in the working lane if their team service area is busy. Built-in AI cars also request stops from tire condition: below `pitStops.tirePitRequestThresholdPercent` they request `pitIntent: 1`, and below `pitStops.tirePitCommitThresholdPercent` they request `pitIntent: 2`. Expert-controlled cars do not receive those tire-threshold automatic calls; their model or host must request the stop with `pitIntent`. Hosts and expert actions can change a pending stop with `pitIntent`: `0` clears the pending call, `1` stays active until a free-enough pit-entry window appears, and `2` commits to entering at the next pit-entry window even if capacity or gap checks would block mode `1`. The same request may include a target compound, either through `setPitIntent(driverId, intent, targetCompound)` or expert action `pitCompound`; invalid compounds are rejected. Pit intent and target compound are locked while the car is entering, queued, servicing, or exiting, and intent resets to `0` after pit exit. Completed stops can be re-armed by later tire condition or host intent, so pit stops are not one-use per race. Safety-car mode does not block pit intent. Closed pit-lane race-control state keeps pending calls on track until `setPitLaneOpen(true)`, while red-flag state freezes cars and closes the effective pit lane until cleared. If the gap is too small for mode `1`, the following car stays on the main track and retries later rather than being forced to stop in the lane. Allowed cars steer through a forward main-track approach into the pit-entry road, slow to the configured limiter speed before the main pit-lane start, follow the main fast lane, pass through the team-colored queue point, hold for any pit-served penalties, hold for the resolved tire-service time, change to the requested compound or default alternate compound, add that compound to `usedTireCompounds`, then steer back to the racing surface through `pit-exit`. While a car is stationary in service, the browser renderer shows a small countdown above it: red `+Ns` for pit-served penalty hold time and yellow `Ns` for normal pit-service time. The queue point is a rolling gate when the active service area is free, so the car continues into service without a full stop. If a team-mate is already servicing or still exiting from the active service area, the arriving car targets the queue point behind that service area, holds a bounded approach speed until the final capture distance, waits without obstructing the main fast lane, then follows a short queue-release route only after the active service area is physically clear; this keeps both the queue stop and the move into service continuous instead of snapping the car between slots or crawling through the last several meters. Cars whose position is controlled by pit entry, queue, service, or exit logic are not laterally displaced by generic collision correction; if another car overlaps one, the movable car is corrected away while the pit-controlled car stays on its authored route or stop point. `pitStops.variability.enabled` optionally adjusts service time from `team.pitCrew.speed`, `team.pitCrew.consistency`, and `team.pitCrew.reliability`; `pitStops.variability.perfect: true` forces `pitStops.defaultStopSeconds` for training. The pit limiter applies only on the straight main pit lane and working lane between the lane start and lane end; the pit-entry connector road and pit-exit connector road are legal pit-lane surfaces but are not speed-limited by `pitStops.pitLaneSpeedLimitKph`, although automatic entry routing caps connector overspeed so worn-tire cars can still reach the limiter and assigned service area.
356
+ When pit stops are enabled, each team is assigned one shared service area in the working lane plus two garage boxes. Driver pairs share the service area, and the service area, queue slot, and garage boxes inherit the team color from `driver.team.color` when present, otherwise the lead driver's color. The current automatic stop plan distributes pit calls across available race laps as bounded pit trains, then lets opportunistic cars join the entry only when fewer than `pitStops.maxConcurrentPitLaneCars` are active and the nearest active pit-lane car is at least `pitStops.minimumPitLaneGapMeters` ahead; committed cars enter at the next pit-entry window and can queue in the working lane if their team service area is busy. Built-in AI cars also request stops from tire condition: below `pitStops.tirePitRequestThresholdPercent` they request `pitIntent: 1`, and below `pitStops.tirePitCommitThresholdPercent` they request `pitIntent: 2`. Expert-controlled cars do not receive those tire-threshold automatic calls; their model or host must request the stop with `pitIntent`. Hosts and expert actions can change a pending stop with `pitIntent`: `0` clears the pending call, `1` stays active until a free-enough pit-entry window appears, and `2` commits to entering at the next pit-entry window even if capacity or gap checks would block mode `1`. The same request may include a target compound, either through `setPitIntent(driverId, intent, targetCompound)` or expert action `pitCompound`; invalid compounds are rejected. Pit intent and target compound are locked while the car is entering, queued, servicing, or exiting, and intent resets to `0` after pit exit. Completed stops can be re-armed by later tire condition or host intent, so pit stops are not one-use per race. When tire condition automatically re-arms a completed stop, it chooses a different available compound instead of silently reusing the current tire; an explicit host/model target compound is preserved. Safety-car mode does not block pit intent. Closed pit-lane race-control state keeps pending calls on track until `setPitLaneOpen(true)`, while red-flag state freezes cars and closes the effective pit lane until cleared. If the gap is too small for mode `1`, the following car stays on the main track and retries later rather than being forced to stop in the lane. Allowed cars steer through a forward main-track approach into the pit-entry road, slow to the configured limiter speed before the main pit-lane start, follow the main fast lane, pass through the team-colored queue point, hold for any pit-served penalties, hold for the resolved tire-service time, change to the requested compound or default alternate compound, add that compound to `usedTireCompounds`, then steer back to the racing surface through `pit-exit`. While a car is stationary in service, the browser renderer shows a small countdown above it: red `+Ns` for pit-served penalty hold time and yellow `Ns` for normal pit-service time. The queue point is a rolling gate when the active service area is free, so the car continues into service without a full stop. If a team-mate is already servicing or still exiting from the active service area, the arriving car targets the queue point behind that service area, holds a bounded approach speed until the final capture distance, waits without obstructing the main fast lane, then follows a short queue-release route only after the active service area is physically clear; this keeps both the queue stop and the move into service continuous instead of snapping the car between slots or crawling through the last several meters. Cars whose position is controlled by pit entry, queue, service, or exit logic are not laterally displaced by generic collision correction; if another car overlaps one, the movable car is corrected away while the pit-controlled car stays on its authored route or stop point. `pitStops.variability.enabled` optionally adjusts service time from `team.pitCrew.speed`, `team.pitCrew.consistency`, and `team.pitCrew.reliability`; `pitStops.variability.perfect: true` forces `pitStops.defaultStopSeconds` for training. The pit limiter applies only on the straight main pit lane and working lane between the lane start and lane end; the pit-entry connector road and pit-exit connector road are legal pit-lane surfaces but are not speed-limited by `pitStops.pitLaneSpeedLimitKph`, although automatic entry routing caps connector overspeed so worn-tire cars can still reach the limiter and assigned service area.
331
357
 
332
358
  ## Contact And Collision Handling
333
359
 
@@ -363,3 +389,7 @@ These are not currently implemented:
363
389
  - Race finish ceremony.
364
390
  - Multiplayer controls.
365
391
  - Official timing rules, licensing, or real-world team identities.
392
+
393
+ ## Penalty Snapshot Ownership
394
+
395
+ Public penalty snapshots copy supported consequence and diagnostic arrays. Changing a returned consequence cannot change live pit-service seconds or enforcement; future ledger updates likewise do not mutate earlier snapshots. This ownership rule does not alter penalty calculation or service sequencing.
@@ -43,7 +43,10 @@ while the vector oracle uses schema names and object fields rather than calling
43
43
  the same active observation values. Debug-only precision may exist only when
44
44
  clearly labeled and must not be presented as model input.
45
45
  - `getObservationSpec()` is the canonical schema for compact loops, including
46
- per-driver sensor overrides.
46
+ per-driver sensor overrides. Surface fields use canonical `kerb`, then
47
+ `illegalSurface` order when enabled, regardless of configuration order.
48
+ `environmentObservationSchema.test.js` covers this across full/compact,
49
+ schema-free, array/Float32, and per-driver override paths.
47
50
 
48
51
  ## Remaining Formal Limits
49
52
 
@@ -12,6 +12,12 @@ The host website owns only:
12
12
  - Optional display/config overrides.
13
13
  - `onDriverOpen(driver)` navigation behavior.
14
14
 
15
+ Opt-in [Grid Walk](grid_walk.md) is package-owned presentation around normal browser playback. Both returned APIs expose `getView()` and `setView('grid-walk' | 'race')`; `gridWalk` options control enablement, initial view and ambient motion. Entry metadata supplies names, summaries and ordinary links for the shared race project banner at the bottom; the entry selector sits centered at the top with centered text. There is no additional project inspector. Grid Walk holds the race before its lights sequence and pauses/resumes exact progress on later returns. Resizing changes camera framing. Drag/touch, wheel and keyboard browsing share one axis-constrained camera action and preserve a bounded longitudinal world position without stepping the race. Measured chrome bounds and packed label lanes prevent overlay collisions; selection does not reset visible framing. Grid labels use opaque themed surfaces; the shared banner follows existing sizing and enabled/muted policy.
16
+
17
+ The package-styled Grid Walk / Race switch mounts outside the scene through `mountViewControls()`, with automatic composition above the all-in-one shell and race telemetry drawer. A standalone canvas requires an explicitly mounted switch or host controls calling `setView()`. `ui.viewControls` defaults to `'switch'` and accepts `'toggle'` for a single green Start race / red Stop race action using semantic green/red flag colors; unknown values use `'switch'`. Stop pauses exact race progress and shows the original display grid, while Start resumes it. Both variants share runtime state, enable when ready, hide when Grid Walk is disabled and update after restart. Wheel browsing yields to page scrolling at the grid limits; Ctrl/Meta wheel retains browser zoom, and Race zoom is excluded while Grid Walk owns input. Varied groups of two to five mechanics and continuously distributed walking visitors respect reduced motion and host ambient-motion settings, with no motion toggle in the overlay. Expert mode is excluded; existing consumer defaults are preserved.
18
+
19
+ Grid Walk releases drag capture on window blur, document hiding and a pointer move with no pressed buttons. Interrupted gestures do not resume when focus returns; subsequent presses start normally. These lifecycle listeners belong to the canvas host's document/window and are removed with the runtime.
20
+
15
21
  ## Public API
16
22
 
17
23
  Main import:
@@ -51,7 +57,7 @@ Root package exports:
51
57
  | Export group | Public exports | Use |
52
58
  | --- | --- | --- |
53
59
  | Browser mounts | `mountF1Simulator`, `createPaddockSimulator` | all-in-one shell or composable package-owned UI surfaces |
54
- | Composable helper mounts | `mountRaceControls`, `mountCameraControls`, `mountSafetyCarControl`, `mountTimingTower`, `mountRaceCanvas`, `mountTelemetryPanel`, `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetrySectorBanner`, `mountTelemetryLapTimes`, `mountTelemetrySectorTimes`, `mountRaceTelemetryDrawer`, `mountCarDriverOverview`, `mountRaceDataPanel` | function-call wrappers around a composable simulator controller |
60
+ | Composable helper mounts | `mountRaceControls`, `mountCameraControls`, `mountViewControls`, `mountSafetyCarControl`, `mountTimingTower`, `mountRaceCanvas`, `mountTelemetryPanel`, `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetrySectorBanner`, `mountTelemetryLapTimes`, `mountTelemetrySectorTimes`, `mountRaceTelemetryDrawer`, `mountCarDriverOverview`, `mountRaceDataPanel` | function-call wrappers around a composable simulator controller |
55
61
  | Startup placeholder | `createPaddockLoadingPlaceholder` from `@inventure71/paddockjs/placeholder`, plus `@inventure71/paddockjs/placeholder.css` | optional pre-JS loading placeholder that does not import PixiJS, simulator assets, root runtime CSS, or the browser mount |
56
62
  | Data helpers | `DriverData`, `VehicleData`, `CHAMPIONSHIP_ENTRY_BLUEPRINTS`, `DEMO_PROJECT_DRIVERS`, `buildChampionshipDriverGrid`, `formatDriverNumber`, `normalizeSimulatorDrivers` | demo data, host entry normalization, and rating helper construction |
57
63
  | Theme helpers | `DEFAULT_PADDOCK_THEME`, `PADDOCK_THEME_CSS_VARIABLES`, `PADDOCK_THEME_TOKEN_KEYS`, `resolvePaddockTheme`, `applyPaddockTheme` | host-side theme preview/sync code without copying package token names |
@@ -132,6 +138,8 @@ const simulator = await mountF1Simulator(root, {
132
138
  });
133
139
  ```
134
140
 
141
+ Race-control headings use a content-sized line box that accommodates wrapped text and native font metrics. Their title and button groups must fit standalone and composed mounts without clipping or overlap; no fixed title-height or overflow masking is used to hide extra text.
142
+
135
143
  The all-in-one shell normalizes `backLinkHref` before rendering it. Package-owned race controls accept relative URLs, hash URLs, and absolute `http:` / `https:` URLs; unsafe or malformed values fall back to the default `projects.html` link instead of being rendered into the anchor. Runtime readouts also validate snapshot-derived color values before writing CSS custom properties, and package-owned asset URLs are escaped before becoming CSS `url(...)` values.
136
144
 
137
145
  Composable mount call:
@@ -162,6 +170,7 @@ const simulator = createPaddockSimulator({
162
170
 
163
171
  simulator.mountRaceControls(controlsRoot);
164
172
  simulator.mountCameraControls(cameraControlsRoot);
173
+ simulator.mountViewControls(viewControlsRoot);
165
174
  simulator.mountSafetyCarControl(safetyCarRoot);
166
175
  simulator.mountTimingTower(timingRoot);
167
176
  simulator.mountRaceCanvas(canvasRoot, {
@@ -195,6 +204,7 @@ The controller methods above are the canonical composable API. They keep compone
195
204
  ```js
196
205
  mountRaceControls(root, simulator);
197
206
  mountCameraControls(root, simulator);
207
+ mountViewControls(root, simulator);
198
208
  mountSafetyCarControl(root, simulator);
199
209
  mountTimingTower(root, simulator);
200
210
  mountRaceCanvas(root, simulator, {
@@ -222,6 +232,7 @@ Returned controller:
222
232
  // Included on composable controllers only:
223
233
  mountRaceControls(root),
224
234
  mountCameraControls(root),
235
+ mountViewControls(root),
225
236
  mountSafetyCarControl(root),
226
237
  mountTimingTower(root),
227
238
  mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit }),
@@ -253,6 +264,9 @@ Returned controller:
253
264
  getPitIntent(driverId),
254
265
  getPitTargetCompound(driverId),
255
266
  getSimulationSpeed(),
267
+ setTimingGapMode(mode),
268
+ getTimingGapMode(),
269
+ toggleTimingGapMode(),
256
270
  servePenalty(penaltyId),
257
271
  cancelPenalty(penaltyId),
258
272
  callSafetyCar(),
@@ -266,11 +280,19 @@ Returned controller:
266
280
  ## Required Behavior
267
281
 
268
282
  - Mounting creates the simulator shell inside the provided root.
283
+ - A root may own only one live all-in-one mount. A second mount is rejected until the first controller is destroyed; failed initialization clears and releases the root for retry.
269
284
  - Composable mounting can place controls, timing tower, canvas, telemetry, car/driver overview, and race-data panels into separate host roots.
270
285
  - Composable mounting can also place camera controls and a safety-car button into separate host roots while keeping package-owned markup.
271
286
  - Composable mounting can embed the timing tower directly inside the race canvas with `mountRaceCanvas(root, { includeTimingTower: true })`.
272
287
  - The race canvas is required before `start()` because PixiJS needs a canvas host.
273
288
  - Mount component roots before calling `start()`. A pre-start `restart(nextOptions)` re-resolves options and refreshes already mounted component markup, so non-asset changes such as title, kicker, UI visibility, theme, drivers, entries, seed, or track seed are reflected before `start()` binds the runtime.
289
+ - `restart()` during pending composable startup is rejected before options or mounted DOM change. Await `start()` (or its rejection) before restarting.
290
+ - Each composable surface key owns one root. Replacing that surface clears its previous root, and different surface keys cannot share one root. Concurrent `start()` calls await one initialization; failure destroys partial state and leaves the controller retryable.
291
+ - Destroying a composable controller during `start()` cancels that start promise. Pending renderer resources are released once initialization settles, and late assets cannot repopulate the unmounted scene or emit ready callbacks. After the cancelled promise settles, hosts may remount components and start again. Cleanup failures are reported through `onError(error, { phase: 'destroy' })` without abandoning independent cleanup or masking an earlier initialization error.
292
+ - Hosts that constrain a standalone telemetry stack should give its root a definite height; the stack then owns internal scrolling. A maximum height alone does not resolve the child's percentage height and can allow content to overflow neighboring host surfaces.
293
+ - Repeated overview, timing, restart and zoom surfaces share one runtime but bind/render independently. Timing rows activate with native mouse, touch and keyboard clicks; wheel zoom ignores horizontal-only and no-op events, modifier wheel input, and Grid Walk-owned input. Project Open callbacks use the standard `onError` isolation contract.
294
+ - Host race callbacks may destroy or restart the simulator. Normal frames, expert frames and readout updates stop before rendering stale state when those callbacks unmount or replace the current race.
295
+ - Normal visible playback caps actual Pixi drawing at 60 FPS, including on high-refresh displays; elapsed-time fixed-step simulation still runs at the requested playback speed. Pixi stays stopped during asset loading, and independent sprite/track textures load concurrently. Expert rendering remains explicitly driven by expert reset/step calls.
274
296
  - `mountF1Simulator()` and each composable `mount*()` call must synchronously write lightweight package HTML before awaiting runtime initialization. Those startup templates may contain controls/readout placeholders, but they must not require PixiJS, texture loading, race stepping, or host-provided hidden DOM to be visible.
275
297
  - Timing tower, telemetry, controls, and race-data panels are optional from a runtime safety perspective; omitted panels simply do not render their readouts.
276
298
  - Timing tower entries are compact content-sized rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count. `ui.timingEntryVerticalPadding` defaults to `5` and applies that pixel value above and below every row entry.
@@ -283,10 +305,10 @@ Returned controller:
283
305
  - `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
284
306
  - `theme` is the public sizing/color contract. It resolves semantic package-owned tokens into complete light/dark themes, supports named derivative theme packages through `theme.themes`, selects the active package with `theme.use`, applies component-specific packages through `theme.componentThemes`, and accepts team selectors through `theme.teamThemes` or `entries[*].team.theme`. Component theme keys accept package component ids such as `timing-tower` and camelCase aliases such as `timingTower`; `selectedDriverPanel` targets both selected-driver package surfaces. Selected-driver surfaces use the selected team's theme by default when one is available while structural controls keep the active global theme unless explicitly overridden. Unknown theme tokens or component slots are ignored, one-sided light/dark token overrides generate and cache the opposite mode, and theme plus driver/team colors are validated before they are written to CSS variables. Legacy aliases such as `accentColor`, `greenColor`, and `yellowColor` remain migration aliases for the new semantic tokens.
285
307
  - `setTheme(theme)`, `setThemeMode(mode)`, and `syncThemeFrom(element, options)` are runtime presentation APIs. They must resolve through the same theme normalizer used at mount time, reapply CSS variables to every package-owned mounted root, and must not call `restart()` or replace race state. Existing simulation state, timing order, selected driver, active penalties, camera state, speed setting, and pit intent state remain owned by the running app. `syncThemeFrom()` applies immediately, watches the configured attribute with `MutationObserver` when available, maps host attribute values through `options.map`, and returns a cleanup function.
286
- - `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'driver'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds with package-owned padding and pit-lane extent. The driver camera follows the selected car from a lower screen anchor and rotates the world so the selected car points upward; its control is opt-in through `ui.driverCamera: true`, and `initialCameraMode: 'driver'` enables that control automatically. The pit camera frames the operational `track.pitLane` lane, boxes, service areas, and queue areas instead of the longer entry/exit access roads, zooms out when needed to keep that pit-lane work area inside the active race-view safe area, and its control is hidden/disabled if the active track has no pit lane. Zoom buttons and wheel zoom work in every camera mode, including overview, show-all, driver, and pit, but cannot zoom farther out than the active track frame.
308
+ - `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'driver'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds with package-owned padding and pit-lane extent. The driver camera follows the selected car from a lower screen anchor and rotates the world so the selected car points upward; its control is opt-in through `ui.driverCamera: true`, and `initialCameraMode: 'driver'` enables that control automatically. The pit camera frames the operational `track.pitLane` lane, boxes, service areas, and queue areas instead of the longer entry/exit access roads, zooms out when needed to keep that pit-lane work area inside the active race-view safe area, and its control is hidden/disabled if the active track has no pit lane. Zoom buttons and wheel zoom work in every camera mode, including overview, show-all, driver, and pit, but cannot zoom farther out than the active track frame. Wheel events are canceled only when the Race zoom value changes; events at either zoom limit can scroll the containing page. Ctrl/Meta wheel, host-handled and non-cancelable wheel events remain browser/host-owned. Native touch scrolling is enabled on Race canvases; only active Grid Walk opts into touch dragging. The runtime never locks document/body scrolling.
287
309
  - Camera controls default to external placement so they do not cover the race view. They can still be embedded in the race canvas by setting `ui.cameraControls: 'embedded'`, externally mounted, or omitted with `false`. Generated camera controls include a `Mute banners` toggle that is off by default and temporarily suppresses project/radio lower-thirds while active. The driver camera button is omitted unless `ui.driverCamera` is active.
288
310
  - Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. It is an explicitly mounted independent surface, not the default telemetry-drawer lower-third. `mountTelemetryPanel()` is the stack template around those detached pieces, owns vertical scrolling when constrained, and `ui.telemetryModules` controls which pieces appear in stack/drawer templates.
289
- - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: external top controls, race canvas, embedded timing tower, lower-third banner, top steward message, safety-car control, and a right-side telemetry drawer. The top row holds camera controls, banner mute, safety car, and telemetry toggle outside the canvas. On narrow responsive containers, that toolbar stacks into readable full-width rows and keeps safety car plus telemetry as a two-column action row instead of squeezing them beside the camera controls. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, is inert/hidden to interaction when closed, and embeds the same `telemetry-stack` component used by standalone telemetry panels.
311
+ - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: external top controls, race canvas, embedded timing tower, lower-third banner, top steward message, safety-car control, and a right-side telemetry drawer. The top row holds camera controls, banner mute, safety car, telemetry toggle and, when Grid Walk is enabled, its separately composed view switch outside the canvas. Camera controls fill their available row, and changing Start/Stop or Telemetry/Close labels reserve their widest intrinsic width to keep adjacent controls stable. On narrow responsive containers, that toolbar stacks into readable full-width rows and keeps safety car plus telemetry as a two-column action row instead of squeezing them beside the camera controls. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, is inert/hidden to interaction when closed, and embeds the same `telemetry-stack` component used by standalone telemetry panels.
290
312
  - The FPS readout can be shown or hidden with `ui.showFps`.
291
313
  - `ui.layoutPreset: 'left-tower-overlay'` is a package-owned preset that creates a left broadcast gutter inside the race view, places the timing tower there at the same width as the default timing-board column, frames the PixiJS camera around the remaining usable race area, and keeps camera controls and start lights out of the tower area. In the combined shell, project and radio lower-thirds stay inside the race window while being allowed to cover the timing sidebar.
292
314
  - `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear. The runtime banner mute state temporarily disables project/radio lower-thirds without changing these configured allow-lists.
@@ -304,11 +326,11 @@ Returned controller:
304
326
  - The host passes data, not internal DOM.
305
327
  - Host driver IDs and entry `driverId` values must be unique. Entries may omit `driverNumber`; provided numbers must be unique.
306
328
  - `totalLaps` is normalized to a finite positive integer before simulation so invalid input cannot produce zero-lap, negative-lap, or non-finite snapshots.
307
- - `physicsMode` accepts `'arcade'` and `'advanced'`. The default is `'arcade'` to preserve existing hosts. `'advanced'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling traction-budget limits, steering scrub, velocity-heading slip, surface-specific grip/drag, and advanced telemetry. The old `'simulator'` mode name is removed and falls back to `'arcade'` like any other invalid value. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
329
+ - `physicsMode` accepts `'arcade'` and `'advanced'`. The default is `'arcade'` to preserve existing hosts. `'advanced'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling four-contact tire forces, load transfer, yaw inertia, aerodynamic load/drag, velocity-heading slip, and advanced telemetry. The old `'simulator'` mode name is removed and falls back to `'arcade'` like any other invalid value. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
308
330
  - `restart(nextOptions)` can change race data and deterministic seeds such as `trackSeed`, but it does not support changing asset URLs. Asset changes require `destroy()` and a fresh mount because PixiJS texture loading is an initialization boundary.
309
331
  - `onDriverOpen(driver)` is the navigation boundary.
310
- - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Lifecycle callback failures are routed to `onError` when possible and must not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback.
311
- - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. Destroyed/out-of-race cars expose `dnf`, `dnfReason`, `dnfAt`, and `dnfOrder`, appear below active cars in timing, render faded/gray in the race canvas, and do not block race completion while they remain DNF. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every race participant has finished or is DNF. DNF entries are included after finishers with no finish time. A DNF car restored before final classification re-enters live timing and must finish; after final classification, resurrection does not reopen the race. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts finishers by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior.
332
+ - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Normal ticker frames and expert-driven frames use the same lifecycle emitter. Stable event identifiers distinguish separate same-time events, while duplicate delivery of the same event is suppressed. Lifecycle callback failures are routed to `onError` when possible and must not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback.
333
+ - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. Destroyed/out-of-race cars expose `dnf`, `dnfReason`, `dnfAt`, and `dnfOrder`, appear below active cars in timing, render faded/gray in the race canvas, and do not block race completion while they remain DNF. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every race participant has finished or is DNF. DNF entries are included after finishers with no finish time. A DNF car restored before final classification re-enters live timing and must finish; after final classification, resurrection does not reopen the race. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts finishers by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior. Timing presentation continues to show final winner/finished labels from `raceControl.finished` rather than reverting to safety-car labels.
312
334
  - Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Active advanced modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, and penalties. Weather, reliability, and fuel load are reserved module placeholders only; they do not currently change grip, power, retirement risk, mass, or pace. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on tracks whose resolved generation options enable it, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1% unless `rules.modules.tireDegradation.enabled` is `false`, can retire cars that remain off legal racing/pit surfaces below the configured stalled-DNF speed threshold for the configured duration when `rules.modules.stalledDnf.enabled` is `true`, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements. `tireStrategy` does not enable automatic pit stops; `pitStops.enabled` is required for pit routing and non-clear pit intent requests.
313
335
  - Penalty subsections use `strictness` from `0` to `1`, not only boolean enablement. `strictness: 0` means the subsection is not enforced; `strictness: 1` applies the configured rule margin. Steward decisions are exposed as `penalty` events and as top-level `snapshot.penalties`. Penalty entries include normalized consequences and lifecycle status. Immediate consequences apply time, grid, position, or disqualification effects directly; drive-through and stop-go consequences remain issued until served, cancelled, or converted at final classification.
314
336
  - The browser UI can opt into top steward messages with `ui.penaltyBanners` and timing-row penalty badges with `ui.timingPenaltyBadges`. Steward messages render track-limit warning events and penalty decisions from the simulation; time-penalty messages put the penalty seconds in a large left chip and use penalty-colored backgrounds, while warnings use warning-colored backgrounds. Timing-row `!` badges are rendered only from `snapshot.penalties`; warning events do not count. UI code must not recalculate steward decisions.
@@ -317,14 +339,15 @@ Returned controller:
317
339
  - Package CSS must be scoped to package mount roots/components and must not style same-named host elements outside `.f1-sim-component` roots. Generic simulator classes such as `.sim-control`, `.camera-controls`, `.timing-list`, `.telemetry-header`, and `.start-lights` are package internals, not host styling hooks. The stylesheet uses system font stacks by default and must not force remote font requests; hosts that want custom typography should load those fonts themselves and pass theme/CSS-variable overrides.
318
340
  - The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
319
341
  - Warmup is enabled by default for browser mounts, headless environments, and direct simulation creation. It runs on a disposable runtime and must not mutate the visible initial race state; under the default `warmup.policy: 'config-change'`, identical configuration fingerprints reuse cached warmup while seed/config changes rerun warmup automatically.
320
- - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed plus resolved generation options for repeated mounts. Cached procedural definitions are immutable; callers that import `createProceduralTrack(seed, options)` should treat the returned definition as read-only and clone it before custom mutation. `trackGeneration` forwards the same procedural options used by `createProceduralTrack(seed, options)`: `profile`, `length`, `startStraight`, `pitLane`, `shape`, `validation`, and `attempts`. The `race` profile preserves the default full circuit with pit lane; `training-short`, `training-medium`, and `training-technical` are smaller pitless presets intended for training or demos. Explicit option fields override profile defaults after the profile is resolved. Procedural generation traces seeded connected region boundaries, smooths and warps them into centerline controls, then rejects circuits with excessive local heading jumps, turn accumulation, self-intersections, poor clearance, invalid length, or weak shape variation.
321
- - The renderer should target a paced 60 FPS simulation/render loop.
342
+ - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed plus resolved generation options in a bounded eight-entry least-recently-used cache. Repeated contracts reuse immutable generated geometry; evicted contracts regenerate deterministically. Callers that import `createProceduralTrack(seed, options)` should treat the returned definition as read-only and clone it before custom mutation. `trackGeneration` forwards the same procedural options used by `createProceduralTrack(seed, options)`: `profile`, `length`, `startStraight`, `pitLane`, `shape`, `validation`, and `attempts`. The `race` profile preserves the default full circuit with pit lane; `training-short`, `training-medium`, and `training-technical` are smaller pitless presets intended for training or demos. Explicit option fields override profile defaults after the profile is resolved. Procedural generation traces seeded connected region boundaries, smooths and warps them into centerline controls, then rejects circuits with excessive local heading jumps, turn accumulation, self-intersections, poor clearance, invalid length, or weak shape variation.
343
+ - The renderer targets at most 60 FPS through Pixi's private ticker. Every draw updates the interpolated vehicle poses from measured elapsed wall time rather than rounded frame counts; deterministic physics still advances in fixed steps. Catch-up is bounded to four target frame intervals, and follow-camera damping scales with elapsed time. There is no second draw/update gate that could render stale poses on irregular or high-refresh displays.
344
+ - Stable replay halo appearance reuses its geometry, and stable overlay clearance causes no repeated class/style mutations. Render snapshot ordering and per-observation nearby preparation reuse internal scratch without changing public snapshot freshness or model-facing sense values.
322
345
  - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Runtime visibility must not rely on a single observer state: it should resync from canvas geometry after observer, scroll, resize, focus, and page-show signals, and ticker sync should reconcile the intended runtime state with PixiJS' actual started/stopped state. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
323
346
  - Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
324
347
  - Restart supports race/data/seed changes but does not support asset URL changes or browser expert mode changes. Texture loading and ticker ownership are mount-time boundaries; hosts must destroy and mount again to change either boundary.
325
348
  - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `metrics`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. `steering` is an absolute normalized steering target: `-1` points at maximum left, `0` points at center, `1` points at maximum right, and intermediate values are percentages of the maximum steering angle; the vehicle integrator rate-limits motion toward that target instead of snapping the wheel. `steering`, `throttle`, and `brake` are required for each controlled-driver action. Strict action policy throws on missing or non-finite vehicle controls, while report policy records the error and releases stale manual controls so the built-in driver owns that physics step. `info.drivers[driverId]` exposes per-driver `terminated`, `truncated`, `endReason`, `episodeStep`, and `episodeId` for batched loops; selected-driver resets clear only selected driver truncation/termination and do not rewind global `info.step`. For scoped reset results, `info.controlledDrivers` matches the drivers present in `observation` and `metrics`, while `info.drivers` preserves episode state for all configured controlled drivers. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Barrier-wall contact in both physics modes is measured against the rendered wall's inner face; contact marks the car destroyed/DNF, removes it from active collision/sensor participation, emits a `car-destroyed` event, and terminates that driver's episode with `endReason: 'destroyed'`. Opt-in stalled off-track DNF removes the car from the same active participation systems while keeping `destroyed: false` and terminates controlled-driver episodes with `endReason: 'stalled-off-track'` when `rules.modules.stalledDnf.enabled` is `true`. Reset is still an episode-boundary `resetDrivers()` call. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; `resetDrivers(placements)` may reset selected controlled drivers between episodes without recreating the whole simulation. Partial `reset(options)` calls preserve omitted nested option groups while replacing arrays and explicit scenario placement maps. After placement, selected cars are classified by the same runoff/barrier rules before observations are returned, so cars already inside terminal barrier space report destroyed metrics and stable miss-valued rays instead of doing alive-car far-out ray scans. Both scenario placement and `resetDrivers()` are setup data, not policy actions. `participantInteractions` may make real physics cars non-colliding, sensor-hidden, non-blocking for pit occupancy, or excluded from race order, but those cars still live in `snapshot.cars` and move only through normal physics/control APIs. The `batch-training` profile is non-colliding, sensor-hidden, pit-non-blocking, excluded from race order, and still rendered. `replayGhosts` are separate trajectory-driven entities in `snapshot.replayGhosts`; they are visual/reference overlays and never enter car physics, timing, pit, order, or penalty systems. Replay ghosts are sensor-hidden unless their own sensor flags explicitly opt them into ray or nearby observations.
326
- - Headless environments also accept an optional `externalRenderer` observer hook. It receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`, so external tooling can stream authoritative frames to visualizers without letting that transport mutate simulation state.
327
- - Controller loop code should use `createPaddockDriverControllerLoop()` instead of duplicating browser/headless stepping. Its controller context groups observations by controlled driver id, includes `orderedObservations` for direct batch tensor assembly, exposes metrics, events, cached specs, and reset-driver ids, and keeps per-driver resets as controller reset hooks when the runtime supports `resetDrivers()`. `previousActions` is the last action map actually applied before the current `actions`; on the first controlled frame after reset it is empty, and after repeated frames it reflects the prior physics step rather than pre-copying the current decision.
349
+ - Headless environments also accept an optional `externalRenderer` observer hook. It receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`, so external tooling can stream authoritative frames to visualizers without letting that transport mutate simulation state. The environment reuses the step's authoritative observation/public snapshot for that hook instead of taking a second full snapshot solely for transport.
350
+ - Controller loop code should use `createPaddockDriverControllerLoop()` instead of duplicating browser/headless stepping. Its controller context groups observations by controlled driver id, includes `orderedObservations` for direct batch tensor assembly, exposes metrics, events, cached specs, and reset-driver ids, and keeps per-driver resets as controller reset hooks when the runtime supports `resetDrivers()`. `previousActions` is the last action map actually applied before the current `actions`; on the first controlled frame after reset it is empty, and after repeated frames it reflects the prior physics step rather than pre-copying the current decision. Stopping or fully resetting the loop invalidates an in-flight async decision so it cannot apply a stale action after cancellation.
328
351
  - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection; scratch-backed car-hit checks use the same caller-owned ray vector as the road/surface tracer and scalar local footprint math. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested. `precision: 'driver'` road-edge, kerb, and illegal-surface channels use validated direct indexed ray-band intersections for safe normal-driver main-track geometry and validated off-track recovery boundaries; the `batch-training` profile keeps the accelerated direct indexed contract for safe main-track rays, including surface-only batches that reuse shared surface-hit and track-band boundary scratch. Ambiguous recovery, pit-connector, and pit-lane-origin rays use bounded sampled validation inside the ray-band tracer, and `precision: 'debug'` uses that same tracer path with extra sampled refinement for diagnostics. Only missing-index cases use the older sampled fallback path. This preserves model-facing distances without substituting debug-only refinement. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
329
352
  - Scratch-backed expert ray batches dispatch those requested channels through reusable scalar flags on the shared ray query. This is an internal runtime contract only; public ray objects still expose the same road-edge, kerb, illegal-surface, and car channel objects.
330
353
  - Track surface bands are single-source geometry. Rendering derives visible gravel, runoff, and barrier offsets from the same track widths used by wheel surfaces and ray surface hits: road edge, kerb outer edge, gravel outer edge, runoff outer edge, and barrier wall width. New code must not add independent magic offsets for these bands.
@@ -334,7 +357,7 @@ Returned controller:
334
357
  - Browser expert mode disables automatic ticker-driven simulation advancement. The visual canvas updates only after explicit expert `reset()` or `step(actions)` calls.
335
358
  - While external renderer mode is attached, browser expert runtime is strict render-only and rejects local `step()`, `resetDrivers()`, and `reset()`. Hosts must detach before returning to local stepping; detach restores the current local simulation track surface if external frames had redrawn the shared track asset.
336
359
  - Policy Runner `Live preview stream` mode accepts `ws://`/`wss://` push streams and `http://`/`https://` polling endpoints that return `preview:snapshot` frames.
337
- - Policy Runner `Policy server` mode uses compact HTTP protocol version `2`. Reset/init requests include static specs, configuration, `previousActionFields`, and `metricFields`; per-decision `/policy/decide-batch` requests include `driverIds` plus aligned compact `vectors`, `previousActions`, `metrics`, and `events` arrays only. There is no rich per-decision compatibility toggle in the local-preview policy-server controller; server integrations must read vector inputs by matching `driverIds[index]` to `vectors[index]` and cache the tuple field order received on reset. Full public snapshots remain available through explicit host/debug/export/state APIs, not as the policy-server transport default.
360
+ - Policy Runner `Policy server` mode uses compact HTTP protocol version `2`. Reset/init requests include static specs, configuration, `previousActionFields`, and `metricFields`; per-decision `/policy/decide-batch` requests include `driverIds` plus aligned compact `vectors`, `previousActions`, `metrics`, and `events` arrays only. There is no rich per-decision compatibility toggle in the website Policy Runner controller; server integrations must read vector inputs by matching `driverIds[index]` to `vectors[index]` and cache the tuple field order received on reset. Full public snapshots remain available through explicit host/debug/export/state APIs, not as the policy-server transport default.
338
361
  - Browser expert mode may opt into `expert.visualizeSensors: true` or `expert.visualizeSensors: { rays: true }`. When enabled, ray sensors render in the race canvas world layer from the selected controlled car by default, using the same observation result produced by explicit expert steps. The overlay draws a separate colored marker for each detected active channel on each ray, including road-edge, kerb, illegal surface, and car hits. Barrier walls are visible track geometry, not ray-hit markers, and browser components must not reintroduce a barrier ray label. Hosts may request `visualizeSensors: { rays: true, drivers: 'all' }` for the heavier all-controlled-car overlay or pass an explicit driver id list. Every model-facing sense follows this rule: the model receives the active environment observation, and Policy Runner/expert visualization displays that same observation instead of recomputing a more precise or different browser-only value. Extra diagnostics must be clearly separated from model senses.
339
362
 
340
363
  ## Current Visible Features
@@ -412,3 +435,7 @@ Expected:
412
435
 
413
436
  - The host bundle builds with PaddockJS resolved from npm.
414
437
  - Browser smoke shows shell, canvas, driver rows, FPS readout, and working `Open project` navigation.
438
+
439
+ ## Demo Physics Selection
440
+
441
+ The product demo's automatic races, composable examples, layout presets and headless API example use the package-default `arcade` mode. Advanced physics is explicit in the separate, lazily launched keyboard/sensor lab at `#expert`, visibly labeled Advanced physics. The advanced lab uses the rebuilt planar tire-force model with human keyboard smoothing; it remains an experimental handling approximation. Only a focused, visible race captures driving keys, and blur/hidden/disposal clears held inputs and the keyboard's pending pit request. Package defaults and public mode selection are unchanged.
@@ -7,6 +7,7 @@ PaddockJS reliability work is risk-driven. Do not add tests for coverage vanity.
7
7
  Use the smallest verification layer that proves the behavior, then run the broader gate before claiming package quality.
8
8
 
9
9
  - Unit tests in `src/__tests__/` prove deterministic simulation, data normalization, sensor math, controller loops, and focused UI rendering helpers.
10
+ - Vitest resolves demo package imports to this checkout's public entrypoints. Root tests therefore need only `npm ci` in the package, even before either browser consumer is installed. Packed-consumer checks remain the proof of install/export resolution; test aliases do not replace them.
10
11
  - Public type tests in `src/__tests__/publicApi.types.ts` prove the TypeScript consumer contract for root, `/data`, `/environment`, and `/placeholder` imports.
11
12
  - `npm run consumer:smoke` proves the packed tarball installs and builds in a fresh Vite consumer through public package imports.
12
13
  - `npm run consumer:bundle-boundaries` proves the packed root browser import remains the CSS/asset-owning simulator bundle while `/placeholder`, `/data`, and `/environment` stay CSS-free or lightweight according to their documented roles.
@@ -50,6 +51,10 @@ Use the smallest verification layer that proves the behavior, then run the broad
50
51
 
51
52
  ## Reporting Standard
52
53
 
54
+ For claims about browser smoothness or retained memory, identify the actual GPU renderer, device/canvas pixel density, viewport, physics mode, speed and visibility. Collect sustained draw-interval distributions and update-work timing in a hardware-accelerated browser; smoke screenshots and average FPS alone cannot prove smooth motion. Record long gaps separately from normal jitter. For retention checks, compare post-GC heap, DOM-node and event-listener counts over repeated restart/selection/disclosure or remount cycles, distinguishing bounded warmup/cache growth from persistent growth. Keep instrumentation bounded and restore hooks after sampling. Software-rendered preview results are environment evidence, not hardware-performance acceptance.
55
+
56
+ Compare efficiency changes to the preserved starting source with identical seeded workloads and operation counts. Alternate fresh baseline/candidate processes when timing drift is visible. Exact outputs and cost counters justify semantic and work-reduction claims; report wall-time ranges and machine noise rather than extending one workload's speedup to the whole package.
57
+
53
58
  Every reliability audit should report:
54
59
 
55
60
  - current confidence: what is proven and what remains unproven;
@@ -0,0 +1,108 @@
1
+ # v11 improvement evidence — 2026-10-13
2
+
3
+ This pass improves built-in racecraft, removes redundant high-refresh rendering and preview allocations, and fixes browser startup/callback cleanup. Implementation is isolated on `v11-improvements` in a separate worktree based on `5bd587528a1b95cd78dd77768129e71a2b09a0e6`. The original v11 checkout and ref are preserved. No commit, push, release or publication was performed.
4
+
5
+ ## Subsequent efficiency and maintainability pass
6
+
7
+ The follow-up requested after motion testing is recorded separately in [the measured efficiency audit](efficiency_cleanup_2026-10-13.md). It covers decision-local capacity reuse, render/sensor allocation reduction, observer and timing DOM convergence, ownership cleanup, native GPU frame-time/memory probes and current package gates. Earlier verification tables below remain historical evidence for the first pass.
8
+
9
+ ## Racecraft results
10
+
11
+ The same six production demo fields were run in both physics modes, with ten cars, their normal tires/pit rules, fixed seeds, fixed `1/60` steps and a 1,200-second ceiling. Baseline and final cases each ran twice. Every case finished with all ten cars, zero DNFs, and exact repeated trajectory/event/result hashes. Measurements are simulation seconds, not wall-clock speed.
12
+
13
+ | Mode / field | Contacts before → after | Off-road car-seconds before → after | Full-field finish seconds before → after |
14
+ | --- | ---: | ---: | ---: |
15
+ | Arcade / hero | 50 → 6 | 0 → 0.050 | 724.667 → 716.783 |
16
+ | Arcade / components | 65 → 8 | 2.467 → 2.350 | 501.733 → 500.450 |
17
+ | Arcade / dashboard | 43 → 7 | 0 → 0.450 | 445.800 → 448.350 |
18
+ | Arcade / timing-overlay | 52 → 5 | 0 → 0 | 497.467 → 493.617 |
19
+ | Arcade / compact-race | 57 → 7 | 0.167 → 0.333 | 407.633 → 407.850 |
20
+ | Arcade / full-dashboard | 38 → 6 | 1.067 → 1.017 | 329.283 → 313.917 |
21
+ | Advanced / hero | 12 → 2 | 0 → 0 | 887.817 → 879.133 |
22
+ | Advanced / components | 3 → 0 | 0.300 → 0.050 | 597.350 → 596.767 |
23
+ | Advanced / dashboard | 1 → 0 | 0.467 → 0 | 528.933 → 524.133 |
24
+ | Advanced / timing-overlay | 3 → 0 | 0.767 → 0 | 604.583 → 601.017 |
25
+ | Advanced / compact-race | 3 → 1 | 0.433 → 0 | 486.567 → 483.500 |
26
+ | Advanced / full-dashboard | 6 → 4 | 0 → 0 | 356.183 → 354.667 |
27
+
28
+ Arcade contacts fall from **305 to 39 (87.2%)**; advanced contacts fall from **28 to 7 (75%)**. Advanced off-road time falls from 1.967 to 0.050 car-seconds. Arcade retains a small tradeoff: aggregate off-road time rises from 3.700 to 4.200 car-seconds, and two fields finish slightly later. Its aggregate full-field finish time falls by 25.617 seconds; advanced falls by 22.217 seconds. These are fixed demo fixtures, not a guarantee for every track, rating sheet or traffic arrangement.
29
+
30
+ Off-road time uses the harness's wheel-derived `trackState.onTrack === false` during active running intervals, outside pit-controlled routing and pit-lane travel. Contact counts are physical contact events consumed once per step, rather than penalty consequences counted again. Lap times use canonical timing-line crossings. The harness prints track lengths, source/options hashes, per-car laps, incidents, tire state and pit completion facts.
31
+
32
+ Simulation/data source SHA-256:
33
+
34
+ - Baseline: `a70d95444f064aa70ecea1d407347dffc97403f2ae00f309590f94f92a7c0800`.
35
+ - Final: `2e4c4d81b0ecc12505110f2bd7ed882229c1fa8b1dbb6cf64ab582d63d25a01a`.
36
+
37
+ The final narrow-road comfort-margin correction was also checked against the prior wide-road candidate: all 12 field result hashes remained identical. A separate 405-combination check preserved ordinary-road offset calculations while fixing a six-meter road's negative comfort limit.
38
+
39
+ ## What changed and why
40
+
41
+ - Physical traffic gaps wrap around the circuit. Lapped cars and start-line neighbors can no longer disappear because their classification distances differ by a lap.
42
+ - Traffic uses chassis centers in the main-track frame. Outer-wheel surface offsets and pit-local offsets cannot exaggerate passing clearance. Separate pit traffic is excluded from tactical targets; merging traffic remains relevant.
43
+ - Both modes follow stopped/slow blockers until there is real lateral clearance. Arcade's minimum maneuvering speed no longer overrides the blocker limit.
44
+ - Passing intent no longer fights reactive defense. Drivers avoid defending against a physically rearward lapping car.
45
+ - Arcade holds its line through loaded turns with traffic, retaining inward edge recovery priority. Advanced constrains its final target beside overlapping cars and permits inward correction near the edge.
46
+
47
+ An initial center-space tactical candidate substantially increased off-road time and was rejected. Swept lane constraints at intermediate intention/scoring boundaries also caused arcade regressions and were rejected. Final constraints are applied to the advanced controller's actual composed target; arcade retains its independently verified corner-hold policy. Physics forces, grip, pose integration and model observations were not changed.
48
+
49
+ ## Resource and lifecycle improvements
50
+
51
+ The actual private Pixi ticker is capped at 60 FPS and stays stopped during loading. Real Pixi ticker tests drive synthetic 30/60/120/144 Hz refreshes and check both draw counts and elapsed fixed-step simulation. High-refresh tests produce approximately 60 draws per second instead of 120/144 draws of a scene updated at 60 Hz. This proves reduced draw work; it is not a claim of a measured device-wide CPU/GPU percentage.
52
+
53
+ ### Follow-up: reported car/camera stutter
54
+
55
+ Interactive review exposed a clock defect missed by the original average-FPS checks: simulation advancement rounded elapsed time to whole 16.667 ms frames, leaving almost no interpolation remainder. A second frame gate could also skip pose updates while Pixi still drew the scene. Real-ticker regressions reproduced uneven movement at 30/60/90/120/144 Hz with scheduling jitter. The runtime now updates on every capped draw, accumulates measured wall time with the existing four-frame catch-up ceiling, and renders the fractional fixed-step remainder. Camera target, scale and rotation damping use that elapsed interval. Destroyed/out-of-race cars render at their frozen current pose so their last moving endpoints cannot oscillate with render phase. Physics and AI policies remain unchanged by this follow-up.
56
+
57
+ The new coverage checks each rendered position against elapsed time at 1x and 10x playback, bounds long-stall recovery, checks camera wall-time response, and exercises frozen-car rendering through a real race simulation. The collaborative preview additionally ran through SwiftShader software rendering: 45 sampled animation intervals averaged 106.3 ms (about 9.4 FPS), and its GPU process exceeded eight CPU cores of load. That environment limits visual-performance acceptance; the demo was also opened in the normal desktop browser. The timing fix does not claim to make software rendering reach 60 FPS.
58
+
59
+ Follow-up verification: `npm run check` passed 1,164 fast tests plus its packaging, type, showcase and quick browser/demo checks; the subsequently added frozen-car cases passed in targeted coverage. The final `npm run check:release` passed all **1,259 tests**, four dependency audits, type checking, packed-consumer checks, both showcase builds, and the full browser and demo smoke gates. A live Pixi probe saw 30 draw callbacks, 30 pose updates and zero interpolation error; Overview and Selected camera interactions also passed. Evidence is in `/tmp/paddock-v11-stutter-check.log`, `/tmp/paddock-v11-stutter-release.log`, and `/tmp/paddock-v11-stutter-live-motion.json`.
60
+
61
+ Advanced preview geometry reuses one point across sampled braking decisions; target geometry uses separate reusable scratch. A production sampling test asserts stable buffer identity and repeat speed results. Isolated pre/post scratch substitutions preserved every-step controls and trajectories for 45-second three-car advanced runs on track seeds 12, 7110 and 7203. This equivalence concerns the allocation substitution, not the later tactical changes. Following decisions reuse the lane scan rather than scanning the field a second time.
62
+
63
+ The standard runtime benchmark passes all 23 cases, covering simulation phases, indexed track/pit queries, collisions, sensors, compact environment output, snapshots/JSON, policy transport, interpolation and DOM readouts. Desktop background activity made wall-clock comparisons unsuitable for a reliable speedup claim; acceptance uses the benchmark's cost/identity counters and focused draw/allocation invariants.
64
+
65
+ Pending Pixi initialization now has explicit resource ownership. Destroy waits to release eventual resources after init settles, while a cancelled start cannot append a canvas, restore textures or emit ready. Partial resize/ticker plugin failures release their listeners/tickers as well as the stage/renderer. Independent disposer errors are reported without abandoning the rest of cleanup or masking the original initialization error.
66
+
67
+ Normal/expert frames and readout updates stop when host callbacks destroy or replace the race. Callback dispatch itself captures the simulation and reset generation, preventing remaining obsolete events or finish/lap bookkeeping from corrupting a successor race.
68
+
69
+ Independent sprite and track texture loads start concurrently. Fresh root tests resolve repository-demo imports to the current public source entrypoints; fresh packed consumers separately prove package resolution. The option-normalization test now fixes seed/track seed instead of making random track generation part of its timing/reproducibility.
70
+
71
+ ## Tooling and package boundary
72
+
73
+ Clean installs exposed missing optional peer entries in both browser consumer lockfiles; those are repaired. All three consumer/root locks use the patched `source-map-js@1.2.2`.
74
+
75
+ Changesets v2's release-only dependency chain contained an [unpatched braces advisory](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm). Release tooling is now a pinned, private, nonworkspace Changesets 3.0.3 package. The simulator retains Node `>=20.19.0`; tooling uses its supported newer Node versions, with Node 24 in the release workflow. The root launcher preserves arguments, exit status and repository-root working directory. The release audit checks all four graphs with no exclusions. Local auditing found zero vulnerabilities in each graph.
76
+
77
+ The workflow uses Changesets action v2 with explicit version/publish scripts. `npm publish --provenance` is preserved. Read-only CLI status identifies only PaddockJS's pending minor release, excludes private tooling, and leaves package/lock/changelog/Changesets inputs unchanged. Default changelog resolution was verified from the isolated tool installation.
78
+
79
+ Two preexisting publishing follow-ups remain outside local verification: plain npm publishing supplies no structured Changesets publication events for automatic published-package/tag/release reporting, and the version script does not regenerate release lock metadata automatically. Synchronizing version/lock/changelog inputs and authorizing/testing real release execution remain release-handoff requirements. No versioning or publishing command was run against the live checkout.
80
+
81
+ ## Verification
82
+
83
+ | Gate | Result | Scope |
84
+ | --- | --- | --- |
85
+ | `npm run check` — Node 24.15.0 / npm 11.6.2 | Passed | 67 fast test files; 1,150 passed / 93 intentionally deferred slow tests; types, dry pack, fresh packed consumers, bundle boundaries, both builds, quick showcase smoke and complete demo smoke |
86
+ | `npm run check:release` — Node 24.15.0 / npm 11.6.2 | Passed | All 67 exhaustive test batches / 1,243 tests; four dependency audits; types/pack/consumers/builds; full showcase matrix and complete demo smoke |
87
+ | Minimum Node 20.19.0 / npm 11.6.2 | Passed | Docs, all fast tests (1,150 passed / 93 slow skips), public types and dry pack |
88
+ | Standard runtime benchmark `--verify` | Passed | All 23 benchmark cases on baseline and final source |
89
+ | Exact-source production field matrix | Passed | 24 final executions plus 24 baseline executions; all cars finish, no DNFs, exact within-case repeat hashes |
90
+ | Independent runtime integration review | Passed after fixes | Partial plugin cleanup and stale callback delivery reproduced before fixes and independently rechecked after them |
91
+
92
+ The first broad run hit the option-normalization test's five-second timeout during unseeded track generation under desktop load. Fixing that test's seed/track seed made its inputs deterministic; the focused test and both supported-Node gates passed without increasing its timeout or disabling warmup. Changes after the broad gates were evidence/documentation updates only; final documentation checks and diff whitespace checks passed.
93
+
94
+ Relevant reproducible commands from this checkout:
95
+
96
+ ```bash
97
+ npm run check
98
+ npm run check:release
99
+ node scripts/benchmark-runtime-efficiency.mjs --profile=standard --verify --json
100
+ node scripts/benchmark-advanced-ai-fields.mjs --physics-mode arcade --repeats 2
101
+ node scripts/benchmark-advanced-ai-fields.mjs --physics-mode advanced --repeats 2
102
+ ```
103
+
104
+ Pass the untouched baseline checkout as the field harness's optional first source-root argument to reproduce baseline comparisons. Use a supported release-tool Node version for `check:release`; ordinary package tests remain compatible with Node 20.19.
105
+
106
+ Direct built-browser verification additionally exercised overview/selection, timing gap switching, telemetry open/close, safety-car deploy/clear, and 390-pixel timing reveal/resizing without horizontal overflow. A real Pixi startup was destroyed before completion, released its stage/renderer, left no canvas, delivered no ready/error callback, then retried successfully with one canvas and a 60 FPS ticker. Automated smoke also verifies painted canvases and the broader public controls, expert/headless and preset paths.
107
+
108
+ Subsequent skill-dependent physical-envelope tuning, timing-line boundary repair and measured limitations are documented in [limit driving, 2026-10-14](limit_driving_2026-10-14.md). Earlier results above describe their recorded candidate.