Skip to content

Integrate main with the three feature lines (Q13) - #4

Merged
Onlyti merged 84 commits into
mainfrom
integration/p0-merge
Sep 6, 2026
Merged

Onlyti merged 84 commits into
mainfrom
integration/p0-merge

Conversation

@Onlyti

@Onlyti Onlyti commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Integration of the three independent feature lines onto main (PO Q13, briefing §14).
main and the feature lines had diverged in both directions since 34db50c, so the
resolution happens here rather than on main.

What is merged

# line commits brings
A feat/scene-sensors-parser 68 trace container, headless BEV renderer, comms/scene parsers, AC importer M1, D0–D2 contact work
B feat/render-preset-overview 47 render presets, panel selection, layout violation check
C docs/validation-canonical-config 50 validation preset, currency gate, pinned CMake, GUI v3 test switch

chore/repo-hygiene is already contained in line A and needs no separate merge.

Conflict hunk classification

class hunks how it was resolved
physical baseline (ISO target, benchmark reference, analytical comparison) 0 nothing to resolve — no such hunk conflicted
measurement metadata (test counts, currency block, evidence logs, dates) 20 no number taken from either side; the currency block is PENDING
code, same feature under two names 7 main's generalisation kept (SessionKind over direct_control_path, skip_kinematic_blend() over vp_.plant_path)
code, genuinely different features on the same lines 8 composed, not chosen (contact-frame work + T6 instruments + preset BEV toggles)
build/registration surface 6 union of both test and target lists
historical dated evidence (docs/evidence/review/2026-06-21/**) 15 main-lineage record kept; the thesis-branch copy of the same filename documents a tree not in this history
additive documentation rows 11 union

Total 67 hunks over 21 files across the three merges.

Test counts

docs/VALIDATION.md carries no number. The last canonical figure was measured on a tree
that no longer exists, so it is void rather than stale, and the currency block sits in an
explicit PENDING state with the reason recorded. The gate learned PENDING as a state
(it demands a pending: line, and the duplicate-count scan over docs/ stays active) so
that the block did not have to be deleted or bypassed to get through the merge.

Post-merge fixes in this PR

  1. The build directory name was hardcoded as build in the ctest PYTHONPATH and in
    tools/module_workshop.py, so the canonical preset — which builds into
    build-validation — only worked next to a stale build/. Both now follow
    ${CMAKE_BINARY_DIR}.
  2. D1ContactFrame.InvalidContactResetsTireTransientBeforeRecontact rebuilt a level from
    a State snapshot and required an exact match. That stopped being exact at L4 when
    main added KC bushing compliance, whose history lives in the axle DAE rather than in
    State. The check now measures that reconstruction residual on a control run that never
    loses contact and requires the airborne run to be no worse — no tolerance was fitted to
    the observed failure. L3 remains exact.
  3. tests/fixtures/d0_flat_contact_l4_baseline.csv re-captured, with the cause measured
    rather than assumed: suppressing compliance_targets_rad() makes this tree reproduce
    the pre-merge L4 fixture bit-for-bit, so main 9a40c1b moved it and the contact-frame
    work did not. The L3 fixture is untouched and still bit-identical.
  4. The validation CI job built the Python bindings with no pybind11 and ran the
    catalog/plant/trace tests with no numpy, pyyaml or matplotlib. Both jobs now install the
    same pinned set.

Local result

Clean configure + build + ctest --preset validation on ailab-12: 485 registered,
485 passed, 0 failed. That number is deliberately not written into the document — the
canonical measurement is the CI validation job on this PR.

🤖 Generated with Claude Code

Onlyti and others added 30 commits June 18, 2026 00:29
…iction patch, python wrapper

Implemented via Cursor delegation, VERIFIED by Claude (build clean, 386/386 ctest green).
Deliverables (spec docs/design/VLA_THESIS_PLANT.md):
- seven_dof: direct CmdL1 per-wheel torque path (T_net = motor-brake-fx_kin·Re -> spin ODE),
  bypassing the throttle/drivetrain map -> native MF96 combined-slip = physical κ + grip loss
  (the #0 crux; verified at seven_dof_dynamics.cpp:611). plant_path disables the vehicle
  kinematic blend (tyre VLOW only). drive_split_front in params/yaml.
- create_friction_patch_ground (per-wheel contact-x μ). wheel_mu() getter (L2 override).
- SimSession.direct_control_path (lockstep, no cascade/actuator). pybind make_vla_plant_session,
  make_init_state_6dof, CmdL1/CmdL3.
- configs ioniq5_awd.yaml + ioniq5_pacejka.yaml (per-wheel B=Ca/2 verified, mean B≈14.2).
- python/vdsim_plant.py (boundary validation + clear errors) + README + examples/vla_plant_demo.py.
- tests/integration/test_vla_plant.cpp (CmdL1 not throttle-mapped, friction patch, headline
  brake+turn on patch). Cursor also fixed a dynamic-steering regression it found.

BETA — to evaluate + iterate before any main merge (this is the first client/beta tester).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eptance [BETA iterate]

Beta evaluation found a real model defect: in the direct-L1 path a constant brake_torque kept
decelerating the wheel past omega=0 -> the wheel spun BACKWARD -> kappa ran to -31 (garbage GT)
under hard braking. Fix: brake opposes rotation and vanishes at rest (tanh(omega/eps)), so a
locked wheel settles at kappa~-1 (physical) instead of -inf.

Strengthen the acceptance smokes (Cursor's were loose "0.7*cap OR slip"):
- CmdL1DirectTorqueNotThrottleMapped: prove BYPASS (CmdL1 drive torque accelerates; CmdL4
  throttle=0 does not).
- CombinedBrakeTurnGripLossOnPatch (headline): GT friction-circle invariant ||[Fx,Fy]||<=mu*Fz
  every step + real slip (lock) + brake-no-reverse (kappa>-1.2) + vehicle-level grip loss
  (|sumFx| < commanded). (Over-brake LOCKS -> force on the MF sliding tail below peak, ABS
  territory — so we assert engagement+grip-loss, not peak.)
- GroundTruthConsistency: sum Fy_body ~ m*ay + every wheel inside the circle.

388/388 ctest green. Branch VDSim-Thesis (beta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…me gates [BETA iterate]

Acceptance #1 + #5 verification:
- DryHandlingMatchesLinearBicycle: 7DOF steady yaw rate vs the linear single-track model with
  the same Caf/Car. Measured ratio 0.963 (within 4%) -> confirms the plant reproduces the
  controller's bicycle handling AND the MF96 B=Ca/(C*D) calibration recovered Caf/Car
  (per-wheel B=Ca/2). Gated at 15%.
- FasterThanRealtime: 5 s plant trajectory at substep 5e-4 = ~23 ms wall (gated <500 ms).

390/390 ctest green. Branch VDSim-Thesis (beta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…les)

Add docs/VDSIM_PLANT_TUTORIAL.md — a step-by-step manual a client can follow: build, import
(sys.path), quickstart, the obs schema (units/frames), control [delta, Fx_total], low-mu
patches, full closed-loop drop-in, the ioniq5_awd.yaml + tyre YAML explained (incl. the
Ca->B_lat mapping for new vehicles), API reference, the validated guarantees, tips/gotchas,
and a troubleshooting table. Verified the demo + paths run (python3). README now points to it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…fness + peak-mu [BETA iterate]

Replace the plant tyre MF96 -> MF2002 .tir backend (Cursor, VERIFIED by Claude). MF96 lacked
load dependence of the Pacejka coefficients; MF2002 adds it. Public-synthetic Ioniq5 .tir
calibrated (PKY1=-24.6, PKY2=2.56, PDY1=1.0, PDY2=-0.10, PCY1=1.3; PDX1=1.0, PDX2=-0.10):
- single .tir, but load-dependent Kya(Fz) recovers per-wheel cornering stiffness from the
  front/rear static Fz: Kya(7011)=109.3k, Kya(4558)=79.5k -> axle Caf~218k / Car~159k
  (target 220/160, ~1% — more physical than MF96's mean B).
- cornering stiffness now CONCAVE in load: Kya(2*Fz0)/Kya(Fz0)=1.435 (<2) — the saturation
  MF96 missed (linear Ca). Peak mu falls with load: 0.90 -> 0.81 at 2*Fz0 (PDY2=-0.1).
- friction patch still works (contact mu=0.5 -> peak 0.50; MF2002 honours Lmuy=LMUY*mu_lat).
Wiring: ioniq5_pac2002.{tir,yaml} (backend mf2002 + tir_path), ioniq5_awd.yaml -> mf2002 tyre,
.gitignore exception, vdsim_plant.py resolves tir_path, pybind exposes backend/tir_path. MF96
kept available. 4 new MF2002 tests (real-tyre dry==bicycle, load-sensitivity, patch-halves-grip,
combined grip-loss). 394/394 ctest green. Branch VDSim-Thesis (beta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tyre section now describes the MF2002 .tir backend (load-dependent Pacejka coefficients) that
replaced MF96: load-shaping coeff table (PKY1/PKY2 stiffness saturation, PDY1/PDY2 peak-mu load
sensitivity), single-.tir per-axle Ca recovery from static Fz (Kya(7011)=109.3k->Caf~218k,
Kya(4558)=79.5k->Car~159k), concavity Kya(2Fz0)/Kya(Fz0)=1.435, peak mu 0.90->0.81. Validation
section: 394/394, dry==bicycle for real .tir, load-dependent checks. Branch VDSim-Thesis (beta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-contained handover for the first beta customer (VLA thesis): setup, plant-swap recipe for
closed_loop_sim.py, obs->MPC state mapping, the MF2002 load-dependent tyre change tied to the
sigma_Fz / chance-constraint story, validation status, and the beta-feedback ask. Branch
VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ useGT>1 analysis

Thesis-side beta #1 came back: integration success, slip-angle cleanly separates det 8.3deg
(useGT 1.08) vs antic 2deg (useGT 0.91) — load-dependent MF2002 sharper than the bicycle plant.
Two follow-ups recorded:
- item 0 (requested): tutorial gets a conda/venv rebuild section + notes VDSIM_BUILD_PYTHON
  defaults OFF. Shipped .so is cpython-38; an external MPC env (conda py3.11 acados/cvxpy) needs
  a rebuild with -DPython3_EXECUTABLE / -Dpybind11_DIR for that interpreter (ABI lock).
- useGT>1 diagnosed (NOT a friction-circle violation): wheel.mu reports the contact/road mu,
  not the MF2002 realized peak muy(Fz)=(PDY1+PDY2*dfz)*LMUY*mu_contact. PDY2<0 => a lightly
  loaded wheel (dfz<0) has muy>mu_contact, so |F| can reach ~1.08*mu_contact*Fz. Tyre stays in
  its own load-dependent ellipse; the metric used nominal mu in the denominator. Fix options
  (A expose realized peak coeff / B docs-only) recorded; awaiting decision.
Branch VDSim-Thesis (beta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n A) [BETA iterate]

Thesis beta #1 saw useGT=||[Fx,Fy]||/(wheel.mu*Fz)=1.08 (>1). Diagnosed (Claude): not a
friction-circle violation — wheel.mu is the road/contact mu, but the MF2002 realized peak
muy(Fz)=(PDY1+PDY2*dfz)*LMUY*mu_contact rises ABOVE nominal at low load (PDY2<0), so a lightly
loaded wheel carries ||F|| > mu_contact*Fz while staying inside its own load-dependent ellipse.

Fix (Cursor, verified): add per-wheel realized peak coefficient.
- ITireModel::Output/Wrench gain mu_peak; each backend compute() fills it
  (MF2002 max(|mux|,|muy|); MF96 max(D_long*mu_x, D_lat*mu_y); linear max(mu_long,mu_lat)),
  evaluate() propagates to the wrench.
- IVehicleDynamics::wheel_mu_peak() (Ld2 override), pybind, plant obs wheel[i]["mu_peak"].
- wheel.mu (road mu) preserved. Docs: use mu_peak for the friction circle.

Verified end-to-end through the python plant (combined brake+turn on mu=0.5 patch):
max useGT(old contact mu)=1.046 (reproduces the artifact) -> max useGT(mu_peak)=0.998 (<=1).
New test VlaPlant.RealizedPeakMuBoundsFrictionCircle. 395/395 ctest green. Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… with thesis-side)

Thesis-side independently reached the same mu_peak fix (det useGT 1.08->1.00, matching the plant
probe 1.046->0.998; PDX==PDY so it saturates at exactly 1 at the limit) and confirmed
mu_peak-based friction-utilization fits their P1 gate. Both items closed, no main-merge blocker.
Add a durable Changelog (BETA #1 delivery / BETA #2 feedback round) since the mu (contact) vs
mu_peak (realized) semantics is subtle to track across iterate rounds. Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tance)

Thesis-side low-priority re-request to expose wheel.grip_usage=||F||/(mu_peak*Fz) declined:
plant stays raw-GT-only (usage metric is thesis-side's; tutorial §4). Recorded to avoid
re-litigating next round. Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…utilization caveat

The metric name was misleading: "GT" is the thesis-side GT/Dem source tag (no Dem counterpart
in VDSim, so meaningless to a generic customer), and "use" implies utilization — but the MF
force-slip curve peaks then falls, so sat=||F||/(mu_peak*Fz) is non-monotone: =1 only AT peak
slip, <1 on both the rising side (reserve) and the sliding side (drift, past peak). A bare
sat<1 is ambiguous, so it must be paired with the slip channel to tell reserve from departure.
Rename to "friction saturation ratio (sat)" in tutorial §4 + README and add the disambiguation
table. Historical thesis-side useGT quotes kept as record. Thesis-side code var rename
(useGT/useDem -> satGT/satDem) is their repo (VDSim exposes no metric var — raw GT only).
Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… operating-point characteristics

Replace the derived "sat" ratio guidance with raw tyre operating-point characteristics, so the
analyst computes any metric and — crucially — can tell rising vs sliding (drift) unambiguously,
which a single sat scalar cannot (force-slip curve is non-monotone past the peak).

Per wheel the plant now gives capacity (mu_peak) + peak location (alpha_peak lateral, kappa_peak
long) + current position (alpha, kappa). |alpha| vs alpha_peak => reserve / at-limit / drift.
- mf_peak_slip(B,C,E) (interfaces.hpp, shared): slip at D*sin(C*atan(phi)) peak via Newton on
  (1-E)u+E*atan(u)=tan(pi/2C); C<=1 or B~0 -> inf (no interior peak).
- ITireModel::Output/Wrench + IVehicleDynamics::wheel_alpha_peak()/wheel_kappa_peak() (Ld2);
  MF2002 (By/Cy/Ey, Bx/Cx/Ex), MF96 (B/C/E_lat/long), linear (inf). pybind + plant obs.
- Test PeakSlipMatchesNumericalArgmax (model vs Fy/Fx argmax, Fz 3000/5764/9000, 5%). 396 green.

Verified through the python plant: gentle turn |alpha|0.011<alpha_peak0.140 (reserve);
brake+turn on mu=0.4 |alpha|0.162>alpha_peak0.065 (drift). Dry alpha_peak~=8.0deg matches the
earlier det slip 8.3deg (=at peak). Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ned-slip analysis

The combined-slip peak is direction-dependent (no single scalar), so instead of baking it in,
let the analyst reconstruct the exact combined force surface with the tyre evaluator that is
already bound (create_magic_formula_tire_from_tir -> ITireModel.compute). Better than dumping MF
coefficients: no confidentiality leak (.tir stays in the engine), no re-implementation drift.
- TireOutput now also exposes mu_peak / alpha_peak / kappa_peak (was Fx/Fy/Mz only), so a
  standalone compute() returns capacity + pure peak slips too.
- Tutorial §4: offline tyre-query recipe (combined peak along a slip direction, friction
  ellipse). Verified: pure-lat peak alpha=0.130; at kappa:alpha=1 combined peak alpha=0.079.
396/396 green. Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…re_model)

Previously the only way to query the tyre offline was a fresh create_..._from_tir instance; the
live model the dynamics runs was not reachable (owned by the dynamics, no accessor). Add the
chain: IVehicleDynamics::tire() (const ITireModel*, default null; Ld2 returns its tire_.get()),
pybind .def("tire", ..., reference_internal), and plant.tire_model property. Now the analyst
queries the exact running tyre (single source of truth, no .tir path juggling) for friction
ellipse / combined-slip peak. Verified: plant.tire_model.compute == fresh instance (identical
force law). 396/396 green. Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
….params)

For user data delivery only — the simulation runtime stays flat (VehicleParams + a tyre model
in the dynamics). Add a read-only Python facade mirroring vehicle -> part -> physics:
plant.vehicle.params (VehicleParams), plant.vehicle.tire.params (TireParams),
plant.vehicle.tire.model (live ITireModel; plant.tire_model kept as a shortcut). No C++/runtime
change. Verified the path returns the live model (Fy/alpha_peak match). Branch VDSim-Thesis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Leave a TODO (code + roadmap) to extend plant.vehicle.<part>.{model,params} beyond the tyre to
brake/steering/drivetrain/suspension/ARB. Why: only the tyre has a live model handle today; the
rest are pluggable modules (set_*_module, no getter) or flat VehicleParams fields. Path: add
const module getters on IVehicleDynamics (mirror tire()), bind, add a _PartView each.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ansition + steering lag docs

- P1a: roll_angle_qs() / pitch_angle_qs() → obs['roll'], obs['pitch'] [rad]
- P1b: 2D polygon friction map (point-to-polygon distance, blend_distance=1.0m, min-μ overlap rule)
- P2a: 1D friction_map smooth transition (boundary ±1.0m linear interpolation)
- P2b: steering actuator lag documentation + vp.steer_deadtime_s implementation in SimSession
- Tests: PolygonFrictionInsideAndOutside, PolygonFrictionBlendZone, FrictionPatchSmoothTransition, roll/pitch obs, polygon friction map
- Docs: VDSIM_PLANT_TUTORIAL.md §4 (roll/pitch), §6b (2D polygon), §7 (steering lag), HANDOFF.md updated
- Build: 399/399 ctest green (396 + 3 new)
… ladder

Port thesis core improvements for main cherry-pick: TireSetup per-axle/per-wheel
+ catalog/cosim/pybind + tests; IVehicleDynamics::tire(wheel);
make_direct_control_session + FrictionMapConfig; steer_deadtime via ActuatorModel;
ladder_lowering.hpp; SimOutput wheel GT fields.
Thesis-only plant wrapper: TireSetup, make_direct_control_session,
_obs_from_output(sess.output()) slim path.
Add STRUCTURED_REVIEW spec, evidence bundle tool, and review report.
Fix build_complete, part compat/cards, plant_path default, VLA smoke.
Svelte 5 scaffold (Scenario/Run/Build/Analyze), backend APIs, eval/capture/parity tools, and evidence. GUI tier frozen after this commit until scenario-authoring adoption failures.
Onlyti and others added 27 commits September 1, 2026 13:35
D2 replaces the horizontal projection of gravity with an exact road-frame
resolution and routes the banked-turn centripetal term through the normal
load path instead of an equivalent-ay postprocess.

- seven_dof: build x_road/y_road from the injected unit normal (no small-angle
  linearisation); gravity components are taken along that frame, and the planar
  residual subtracts the legacy Fz*n term so D1's wrench is not double counted.
- seven_dof/fourteen_dof: N/m = g*n_z + (v_x*r)*n_y(body) drives Fz, so a pure
  grade still gives m*g*cos(theta) and a bank redistributes load in the force
  path. v_x*r is frozen per RK substep to keep the +/-bank mirror exact.
- L4 contact height keeps flowing through z_u -> wheel_travel -> the existing
  camber/toe sweep; no layer-specific transform was added.

Analytic gates (raw relative error, printed by the tests):
- static grade 12 deg: a_x rel 1.15e-14, sum Fz rel 2.53e-16 (L3 and L4)
- banked limit speed 10 deg / mu 0.80 / R 60 m: rel 1.37e-16,
  vertical residual -5.46e-12 N (L3 and L4)

D0 flat baseline stays bit-identical on L3 (max abs diff 0). The D1 bank
vertical-delta expectation moves 436.5208463 -> 435.6222462 N because gravity
is now resolved on the exact road-y basis; the D1 closure itself is unchanged.
A trace could not say whether it recorded the plant under verification or
the same code used as an optimiser's internal prediction model. With the
Jacobian in-loop mode the two are the same object, so the hazard is not
in-loop use itself but an undeclared dual role: a run whose plant and
predictor share parameters, presented as a verification result.

manifest.role in {plant, predictor} makes that mechanically detectable
alongside param_hash, which already covers plant parameters only.

- writer emits schema_version 0.2 and role on every trace. role is a
  required keyword-only TraceWriter argument with no default, because a
  default is exactly the silence the field exists to break.
- VDSimPlant.enable_trace defaults to role="plant" — that class is the
  plant; a caller driving it as a predictor passes role explicitly.
- reader resolves role once at open: missing at 0.1 means plant plus one
  warning, missing at 0.2 is an error, an unknown value is an error at
  either version.
- 0.x readability is now an explicit list rather than an exact-minor
  match, so 0.1 stays readable only because the fallback above exists.
- golden fixture regenerated at 0.2; golden_v0_1.vdtrace is frozen in
  place as the artefact that keeps the fallback path under ctest.
- renderer prints the role in the HUD and never branches on it.

Contract: 11_trace_contract_spec.md 3.1 (decided 2026-09-02).
ctest 408/409 in the worktree build; the single failure, cmrosif_compat,
is a pre-existing gap on this branch line (its test script is untracked
here) and is unrelated to this change.
The renderer had one hardcoded screen: the panels came from a fixed
PREFERRED_SERIES list and every BEV layer was always on. Deciding what a
run should show meant editing the renderer.

A preset is renderer configuration, not trace content, so this does not
move schema_version. It declares panel channels, labels, y-ranges and BEV
layer visibility — that is the whole MVP surface; free-form layout
editing stays out of scope.

- vdsim_preset: built-in overview (BEV, driven path, optional reference
  path, speed, steer and longitudinal command), YAML/JSON user presets
  layered over a built-in via extends, and CLI > user preset > built-in
  resolution.
- control, tire_limit and road_contact are refused rather than stubbed.
  A stub would let --preset tire_limit render something that is not the
  contracted screen, and road_contact would advertise contact data the
  core does not resolve yet.
- a channel the trace lacks drops its panel silently, so one preset works
  across runs recorded with different channel subsets; required: true is
  the opt-in that turns the omission back into an error. A trace with no
  plottable channel renders BEV-only instead of failing on an empty grid.
- panel geometry is a function of the panel count alone, and
  layout_violations() checks the drawn figure for text, legend or panels
  leaving the frame. render() returns the list, so the frame guarantee is
  not left to eyeballing a GIF.
- panel series now carry preset labels, so raw channel lookups such as
  u_steer read the stashed channels directly; going through the labelled
  series would have straightened the front wheels on a relabelled panel.

Contract: 11_trace_contract_spec.md 6.3 (approved 2026-09-02).
New ctest render_presets (54 checks). ctest 409/410 in the worktree
build; the one failure, cmrosif_compat, is pre-existing on this branch
line (its test script is untracked here) and unrelated.
…onfig

Q1' of the 2026-09-04 PO briefing: the recorded ctest count is only meaningful
for one named configuration. Two registrations made that count depend on the
checkout instead of the configuration.

- VDSIM_BUILD_GUI_V3_E2E (default OFF) gates gui_v3_e2e. GUI v3 is a deferred
  node-stack component, so its end-to-end test is out of scope for the canonical
  build (07_p0_demo.md 6.3).
- cmrosif_compat is registered only when its driver script exists in the tree.
  It was registered unconditionally while the script is absent on this branch,
  so it could never pass here.

No test logic changes.
VALIDATION.md carried 409/409 measured in a configuration nobody could name,
so the number went stale silently. Fix the record and make staleness a build
failure rather than a reading-comprehension exercise.

- CMakePresets.json: `validation` preset is the single canonical configuration
  (Release, PYTHON=ON, ERGACCESS=OFF, GUI_V3_TESTS=OFF).
- docs/VALIDATION.md: machine-readable currency block carrying all four
  required elements (tests / config / commit / date) plus the excluded
  targets. The three prose copies of the count now name the preset.
  ISO figures and the benchmark matrix are untouched.
- scripts/check_validation_currency.py: parses the block and compares every
  element against `ctest -N` and a ctest run log. Test names are read to end
  of line so gtest value-parameterised names ("... <00-00 00-00>") count.
- tests/scripts/test_validation_currency.py: 22 cases, registered as the
  `validation_currency` ctest, so a broken gate fails the suite.
- build.yml: validation job runs the preset and the gate.
- VDSIM_BUILD_GUI_V3_E2E -> VDSIM_BUILD_GUI_V3_TESTS (default OFF), now
  covering gui_v3_api_smoke as well.

Refs VDSimMarketing 07_p0_demo.md 6.4/6.5, 18_dev_briefing_0903.md Q1'/Q1''.
The validation numbers were measured with whatever CMake happened to be on
the measurement host, while CI ran on a runner label (ubuntu-20.04) that no
longer exists -- every build.yml run since 2026-05-28 was cancelled without
ever being assigned a runner, so the canonical job was not measuring anything.

- move both jobs to ubuntu-22.04 and refresh the compiler matrix to the
  toolchains that image actually ships (gcc-11, clang-14)
- pin CMake to one exact version (3.31.10) in both jobs instead of relying on
  the runner image default; CMakePresets.json is schema v3 and needs >= 3.21
- add `toolchain:` as a required element of the VALIDATION.md currency block
  and fail the gate when the recorded version differs from the one that
  performed the measurement, so a toolchain bump cannot land without the
  numbers being re-measured in the same commit
- re-measure on the pinned CMake: 407/407, three consecutive runs
- drop the stale hand-written counts from README/CONTRIBUTING; the suite count
  lives in docs/VALIDATION.md only
Ancestry alone was too weak a contract for the commit: line. A document
can never record the sha of the commit that contains it, so the gate has
to accept an ancestor -- but any commit in between may add or remove a
test registration, which invalidates the recorded count while every
other element still matches.

The gate now also requires the registration surface (CMakePresets.json,
every CMakeLists.txt, cmake/**) to be byte-identical between the recorded
commit and the measured one. Test sources stay off that surface on
purpose: their effect is already caught by the passed/total comparison.

Every failure now prints the re-measurement command, because an
unexplained gate failure gets "resolved" by deleting the gate.

VDSimMarketing 18_dev_briefing_0903.md 10.1 Q1b / 07_p0_demo.md 6.6 (3).
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
  (00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
  it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
  (it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)

(cherry picked from commit 5ce5b60)
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
  (00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
  it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
  (it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)

(cherry picked from commit 5ce5b60)
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
  (00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
  it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
  (it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)

(cherry picked from commit 5ce5b60)
Integration merge #1 of 3 (PO Q13, integration/p0-merge).

Conflict resolution (20 files, 57 hunks):
- code: main's SessionKind supersedes the feature line's bool
  direct_control_path (same feature, two names); main's
  skip_kinematic_blend() supersedes vp_.plant_path (superset);
  the feature line's D1/D2 contact-frame work is kept verbatim.
- build: union of both test/target lists.
- measurement metadata: docs/VALIDATION.md counts set to PENDING;
  no number is carried over from either side.
- physical baselines: 0 conflicting hunks (ISO/benchmark/analytical
  values untouched).
Integration merge #2 of 3 (PO Q13). Single conflict in python/vdsim_render.py:
line A T6 instrument strip and line B preset BEV-layer toggles are different
features on the same two functions, so both are kept — _setup_axes keeps the
fixed-rect T6 layout and gains line B zero-panel fallback, _draw_static takes
both ax_instruments and bev.
Integration merge #3 of 3 (PO Q13). Conflicts: 4 files, 10 hunks.

- .github/workflows/build.yml: union — main's Python/pybind11 install and
  line C's pinned CMake are independent prerequisites of the same job.
- cosim/CMakeLists.txt: union — main's ac_fast_lane_importer and
  comms_tx_templates kept, cmrosif_compat takes line C's EXISTS guard.
- docs/ROADMAP.md: union of the content rows; every hardcoded ctest count
  is dropped in favour of line C's link to docs/VALIDATION.md.
- docs/VALIDATION.md: no number from either side. The currency block goes
  to the PENDING state — the 407 was measured on a tree that no longer
  exists, so it is void, not stale.

scripts/check_validation_currency.py learns PENDING as an explicit
pre-measurement state so the gate can stay green through the merge without
being deleted or bypassed; it demands a 'pending:' reason, and the
duplicate-count scan over docs/ stays active. 47 unit checks pass.
Three post-merge defects, all real, none worked around by exclusion.

1. Build-directory name was hardcoded. The "validation" preset builds into
   build-validation, but cosim/CMakeLists.txt spelled ${CMAKE_SOURCE_DIR}/build
   into every Python test's PYTHONPATH and tools/module_workshop.py looked for
   build/lib/libvdsim_core.a. Both now follow ${CMAKE_BINARY_DIR}, handed to
   the tests as VDSIM_BUILD_DIR; the documented "cmake -B build" flow is
   unchanged. This is why the preset previously only worked next to a stale
   build/ directory.

2. D1ContactFrame.InvalidContactResetsTireTransientBeforeRecontact compared a
   warm level against one rebuilt from its State snapshot. That is an exact
   clone only if the whole history is in State, which stopped being true at L4
   when main added KC bushing compliance (history lives in the axle DAE). The
   check now measures that reconstruction residual on a control run that never
   loses contact and requires the airborne run to be no worse, instead of a
   tolerance fitted to the observed difference. L3 stays exact.

3. tests/fixtures/d0_flat_contact_l4_baseline.csv re-captured. Attribution was
   measured, not assumed: suppressing compliance_targets_rad() makes the merged
   tree reproduce the pre-merge L4 fixture bit-for-bit, so main 9a40c1b (KC
   bushing compliance) moved it and the contact-frame work did not. L3 is
   untouched and still bit-identical.
The validation job built the Python bindings without pybind11 and ran the
catalog/plant/trace tests without numpy, pyyaml or matplotlib. The trace and
render suites arrive with this merge, so matplotlib and pillow join the build
job pin as well.
VlaPlant.FasterThanRealtime asserted a fixed 500 ms wall-clock budget for 5 s
of simulated time. That budget is the Release contract; an unoptimised Debug
build runs the same code 3-6x slower, so both Debug jobs failed on a number
that says nothing about the plant. Release passed.

The budget is now read from VDSIM_PERF_BUDGET_S, the variable the Python suite
already used, with the parsing in one place (tests/support/perf_budget.hpp).
Unset, empty and malformed all mean "not stated" and fall back to the compiled
default, so a broken CI expression cannot silently disable the assertion. The
Python side is reworded to follow the same rule.

CI injects the value on the Debug row only. Release states nothing and keeps
the strict default, which is where a real performance regression is detected.

docs/VALIDATION.md records what blocked the previous canonical measurement.
The currency block stays PENDING: the figure is not canonical until all five
jobs pass three times in a row.
The three-run CI measurement on integration/p0-merge (run 34035244533,
attempts 1-3) reports 485/485 on all five jobs, so the currency block
moves from PENDING to the five required elements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Onlyti
Onlyti merged commit a0bf9b0 into main Sep 6, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants