Skip to content

feat(swing-sim): land the flight slice of #4466 with its React parity counterpart - #4518

Merged
dieterolson merged 4 commits into
mainfrom
feat/swing-sim-flight-slice
Aug 17, 2026
Merged

feat(swing-sim): land the flight slice of #4466 with its React parity counterpart#4518
dieterolson merged 4 commits into
mainfrom
feat/swing-sim-flight-slice

Conversation

@dieterolson

Copy link
Copy Markdown
Collaborator

Second slice of #4466, after swing_sim.ground (#4517).

Adds 21 swing_sim/flight modules — capability observation and evaluator, ground transfer, ground bounce execution, the regional ground pipeline, surface simulation, spin-axis convention, cancellation — plus the React files that share their contract.

Why Python and React ship together here

The spin-axis change is a parity contract, not an implementation detail. spin_axis_tilt becomes positive_right with atan2(-omega_y,omega_z) in both result_catalog_data.py and ballFlightMetricContract.ts; deliveryDiagnostics drops its inlined formula for the shared spinAxisTiltDeg helper; and ball_flight_metrics_golden_v1.json is regenerated for the new sign.

Landing one side alone would leave the golden fixture disagreeing with one of its two consumers — which is exactly what these fixtures exist to prevent. I caught this by installing the web deps and running vitest rather than trusting the Python suite alone.

A real defect found by the new tests

web/src/model/flight.ts guarded the ground-crossing interpolation with only nextPos[2] < 0. A descending launch starts at height 0, so the crossing is skipped on the first step by t > dt; on the next step the previous point is already below ground, the interpolation fraction goes negative, and tGround lands before zero. The metric contract rejects a negative time, so a descending launch surfaced as:

RangeError: timeS must be nonnegative

…instead of the nonconverged result it is. The guard now also requires the previous point to be above ground, so no crossing is recorded and the evaluator classifies it correctly.

A trap deliberately avoided

flight/tests/test_wind.py keeps main's 1e-9 parity tolerance. The source branch still carries 1e-12, which fails on Linux for the reasons in #4513 — taking the branch's file wholesale would have silently reverted that fix. Symbol-level comparison does not catch it, because the change lives inside a function body.

Likewise flight.ts was not taken wholesale: the branch's copy is +31/−107 against main, i.e. a net reduction, so it would have reverted newer work. Only the targeted guard fix is applied.

Verification

  • 1,163 swing_sim tests pass
  • 1,095 React tests pass across 132 files (full suite, not just the touched ones)
  • tsc --noEmit and eslint clean
  • scoped ruff check / ruff format clean
  • mypy --follow-imports=skip crashes identically with and without this change on main's own flight package — pre-existing, not a regression

Part of epic #4103.

🤖 Generated with Claude Code

Second slice of #4466, after the ground module. Adds 21 `swing_sim/flight`
modules — capability observation and evaluator, ground transfer, ground bounce
execution, the regional ground pipeline, surface simulation, the spin-axis
convention and cancellation — plus the React files that share their contract.

The Python and TypeScript sides ship together because the spin-axis change is
a parity contract, not an implementation detail. `spin_axis_tilt` becomes
`positive_right` with `atan2(-omega_y,omega_z)` in both `result_catalog_data.py`
and `ballFlightMetricContract.ts`; `deliveryDiagnostics` drops its inlined
formula for the shared `spinAxisTiltDeg` helper; and the golden fixture is
regenerated for the new sign. Landing one side alone would have left the
fixture disagreeing with one of its two consumers.

Repairs a real defect in `web/src/model/flight.ts` found by the new evaluator
tests. The ground-crossing guard tested only `nextPos[2] < 0`. A descending
launch starts at height 0, so the crossing is skipped on the first step by
`t > dt`; on the next step the previous point is already below ground, the
interpolation fraction goes negative, and `tGround` lands before zero. The
metric contract rejects a negative time outright, so a descending launch
surfaced as `RangeError: timeS must be nonnegative` rather than the
nonconverged result it is. Requiring the previous point to be above ground
means no crossing is recorded and the evaluator classifies it correctly.

`tests/test_wind.py` deliberately keeps main's 1e-9 parity tolerance. The
source branch still carries 1e-12, which fails on Linux for the reasons in
#4513 — taking the branch file wholesale would have silently reverted that fix.
Symbol-level comparison does not catch this, because the change is inside a
function body.

Verified: 1,163 swing_sim tests pass; the full React suite passes at 132 files
/ 1,095 tests; `tsc --noEmit` and `eslint` clean; scoped ruff and ruff format
clean. `mypy --follow-imports=skip` crashes identically with and without this
change on main's own flight package, so it is not a regression here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@dieterolson
dieterolson enabled auto-merge (squash) August 17, 2026 06:45
codex-scheduled and others added 2 commits August 17, 2026 00:39
Both sides added a 1.17.14 SPEC change-log row. Kept both and renumbered this
branch's entry to 1.17.15. main's row lands the two-tier Python floor contract;
this slice is root-package code under src/shared/python, so the 3.11 floor
applies unchanged and tests/test_python_version_contract.py passes (9 tests).
…line

CI's `Type Check (Mypy)` step reported one error:

    regional_ground_pipeline.py:147:5: error: Returning Any from function
    declared to return "float"  [no-any-return]

`BounceModelSettings` comes from a module that `--follow-imports=skip` does not
load, so its attribute types degrade to `Any` and the value flowed out of a
`-> float` function. Converted explicitly; the settings object has already
validated the number, so this is for the type checker, not the value.

Worth recording why this was not caught before pushing: mypy 1.13.0 on Python
3.13 crashes with `AssertionError: Internal error: unresolved placeholder type
None` when given this file set, so the local run aborted instead of reporting.
CI runs the same mypy on Python 3.12, where it type-checks normally. The file
now passes locally in isolation, which is as close as this interpreter gets.

200 flight tests pass; scoped ruff and ruff format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…support

Two CI failures on the flight slice, both invisible locally.

`tests (3.11)` failed one test with `ImportError: installed tools_core wheel
lacks tee-aware full-state flight; rebuild it`. Rust capability comes in two
tiers and the module-level guard only covers the lower one: a wheel can expose
`simulate_trajectory` — so `is_rust_available()` is true and nothing skips —
while still lacking the tee-aware full-state API, at which point the facade
deliberately raises rather than silently inventing tee geometry. CI has exactly
such a wheel; this machine has no wheel at all, so the whole module skipped and
the gap never showed. The tee test now carries its own `skipif` on the
full-state capability, and the module docstring states both tiers.

`quality-gate`'s Changed Test Assertion Check flagged
`flight/tests/_regional_ground_pipeline_support.py` — a shared request/result
constructor with no assertions by design, the same shape as
`ground/tests/_support.py`. Added to `scripts/test_assertion_allowlist.txt`
beside it.

Verified: 6 parity tests pass (5 run, tee one skips without a full-state
wheel), `scripts/check_test_assertions.py` passes on this branch's changed
files, ruff and ruff format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dieterolson
dieterolson merged commit 26159e2 into main Aug 17, 2026
18 of 19 checks passed
dieterolson added a commit that referenced this pull request Aug 17, 2026
)

Third slice, after ground (#4517) and flight (#4518). Adds six club modules —
assembly binding and its atomic file I/O, engineering sidecar, simulation
adapter, STL export — and splits the plot catalog into `_catalog_entries`,
`_catalog_scalar_entries`, `_catalog_series_entries` and `_catalog_entry_types`.

The catalog split was verified entry-for-entry: 78 identifiers before, 78
after, none dropped. A catalog is data, so a lost entry would not appear in a
public-symbol comparison — counting them was the only way to know the refactor
was faithful.

`plotting/render.py` and `plotting/spec.py` keep main's versions. The source
branch predates the plot point-inspector and series-selection work already on
main, so its copies are a revert, not an update: taking them failed 13 tests
(`assert None is not None`, rendered-pixel counts far below threshold).
Restoring main's two files dropped that to zero.

Makes `rate_of_closure.club` lazily export `assembly_binding`,
`engineering_sidecar` and `simulation_adapter`. All three reach
`shared.python.golf_club`, which transitively pulls
`swing_sim.variation -> solver -> flight -> scipy.integrate`. Importing them at
`__init__` scope meant `rate_of_closure.club.types` — a leaf module of frozen
club specs — dragged SciPy in, because Python runs the package `__init__`
first. That broke `test_ui_contract_submodules_import_without_optional_servers_
or_scipy`, which requires the Morris UI contract to import with scipy, fastapi
and uvicorn blocked. The lazy map follows the shape `swing_sim.ground.__init__`
already uses; all 23 lazy names and all 45 `__all__` entries still resolve.

Verified: 2,538 tests pass across `tests/rate_of_closure` and
`src/shared/python/swing_sim`. The two remaining failures are pre-existing and
Windows-only, both confirmed against a clean tree —
`test_text_reader_normalizes_decoder_resource_errors` (reproduces with these
changes stashed, passes on Linux CI) and `test_new_plot_data_clears_selection`
(a 15 s Qt budget that measures 13.7 s on this box, passes in isolation).
Scoped ruff and ruff format clean.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dieterolson added a commit that referenced this pull request Aug 17, 2026
Fourth slice, after ground (#4517), flight (#4518) and club/plotting (#4519).
Adds 20 modules: capability observation, Morris host/child adapters, regional
ground variation and its control surface, scalar ensemble contract/IO/wire, and
the wind-strategy plot adapter.

Net-new files only. For the 16 files the branch also modifies, main is a
superset and the branch copies are much older — `ensemble_chunks.py` -360,
`_ensemble_parser.py` -340, `plot_definition.py` -312,
`confidence_ellipsoid_mesh.py` -296. Symbol comparison confirms the direction:
`from_json_dict`, `read_plot_definition`, `build_dispersion_metric_variability`
and `apply_global_simulation_values` exist only on main. Taking those files
would have reverted shipped work, which is the same trap that
`plotting/render.py` sprang in #4519 and `test_wind.py` in #4518.

`simulation_adapter.py` genuinely diverges — `run_simulation_ensemble_chunks`
on main against `_TRIAL_FAILURES` on the branch — so it stays at main's version
pending its own reconciliation rather than being guessed at here.

Two `no-any-return` findings from the changed-file MyPy gate are fixed at the
boundary where `--follow-imports=skip` degrades imported types to `Any`:
`capability_observation_ensemble_json` converts explicitly, and `_spin_axis`
unpacks the three components instead of returning the attribute, which also
pins the arity its annotation promises.

Verified: 2,632 tests pass across `tests/rate_of_closure` and
`src/shared/python/swing_sim`. Both changed files type-check clean; ruff and
ruff format clean across the package. The remaining local failures are the
known Windows-only ones already confirmed against a clean tree.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dieterolson added a commit that referenced this pull request Aug 17, 2026
Fifth slice. Adds `ground_playback`, `ground_playback_comparison` and the three
`ground_playback_workspace` modules under `src/rate_of_closure/simulation/`.

This one was genuinely blocked, not merely unscheduled: the modules import
`swing_sim.ground` (#4517), `swing_sim.flight` (#4518) and
`rate_of_closure.club.{assembly_binding,simulation_adapter}` (#4519). With all
three on `main` they now import cleanly — verified by importing each of the five
directly, not only by the suite passing.

Net-new files only, as with every slice. Of the six files the branch also
modifies, `sources.py` is the only branch-superset (`world_from_selected_head`)
and the new modules do not reference it, so every modified file stays at main's
version rather than being taken on the strength of one symbol.

One `no-any-return` finding from the changed-file MyPy gate is fixed where
`--follow-imports=skip` degrades `GroundSimulationResult` to `Any`:
`ground_result_json` converts explicitly, its exact-type precondition already
guaranteeing the runtime value.

Verified: 2,635 tests pass across `tests/rate_of_closure` and
`src/shared/python/swing_sim`; all five new modules import; the Morris UI
contract still imports with scipy, fastapi and uvicorn blocked — the regression
#4519 had to fix in `club/__init__.py` — and the changed-test assertion gate
passes. Scoped ruff, ruff format and per-file mypy clean.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dieterolson added a commit that referenced this pull request Aug 18, 2026
 (#4530)

* feat(rate-of-closure): land the first React web/src/model slice of #4466

101 files: capability observation and result export, club assembly binding and
engineering sidecar wire, ground playback workspace, regional-ground variation
request/workspace/target projection, scalar ensemble contract, wind-strategy
plot data, and their tests.

The React tree cannot be taken wholesale, and the measurement is worth
recording because it is the opposite of what the earlier slices found. This
tree has diverged in *both* directions:

- 129 files exist on main that the branch lacks entirely — the Morris component
  chain (`MorrisWorkflowPanel`, `MorrisResults`, `MorrisFactorEditor`,
  `MorrisWorkspaceActions`), `LaunchMonitorLinkedScatter`,
  `morrisAuthorityProxy`, and 15 Playwright specs, all landed by #4473. This is
  the React mirror of the `application/morris/**` situation in #4524.
- 125 of the 270 modified `model` files delete content on the branch side. The
  worst, `morrisGlobalSensitivityContract.ts`, is +0/-355 — a strict subset of
  main.

I first copied 324 modified files in bulk and then reset it: against a tree
where main demonstrably owns 129 files the branch never saw, that was not a
safe operation, and shipping it would have reverted exactly the kind of work
this whole series has been protecting.

What lands instead is the provably safe subset: files whose main->branch diff
deletes nothing, then an iterative drop of added files whose dependencies need
a main-newer module. That converged in five rounds. `flight.ts` and
`wind.test.ts` are explicitly held at main's version so the #4518 ground-crossing
guard and the 1e-9 parity tolerance (#4513) survive — verified after the fact,
not assumed.

29 added files are deferred. They are not broken; they need their main-side
counterparts, and a later slice can carry both together.

Verified: `tsc --noEmit` clean, `eslint` clean, 1,324 React tests passing across
157 files, production Vite build succeeds. No Python or Rust changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(spec): renumber the web model slice row to 1.17.29

Another agent landed 1.17.28 on main while this branch was open. Renumbered
rather than leaving a duplicate version in the change log.

---------

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dieterolson added a commit that referenced this pull request Aug 18, 2026
…mbol ports (#4541)

Adds 23 net-new files: the regional-ground execution job/result/presentation
and their file IO, the authority client, job-preparation and variation
request wires, the variation workspace, three regional-ground hooks, the
imported-job panel, club STL export, club engineering sidecar, and flight
preparation launch.

Two of the ten symbols blocking the previous slice are ported additively
onto main's versions rather than by taking the branch's files, which delete
content:

  - mesh.writeBinaryStl with binaryHeader and two byte constants; main
    already had BINARY_HEADER_BYTES, BINARY_RECORD_BYTES, triangleNormals.
  - the two regional-ground variation keys, checked byte-for-byte against
    the already-landed regional_ground_variation_dataset.py.

Three candidate ports were investigated and deliberately not made. The
capabilityOptimizer re-exports are dead here -- the only consumer imports
straight from capabilityObservationContract. drawGroundPlayback is built on
branch-local helper signatures whose arities differ from main's, so porting
it means reworking main's renderer to unblock two files. And flight's
angular-state trio would require either duplicating the integrator or
replacing the one carrying #4518's ground-crossing guard.

PrimaryViewTabs.test.tsx and TorqueProfilePanel.test.tsx are both restored
to main's versions: each was purely additive by line count yet failed
against main's unchanged component. The second was caught only because the
prune guard now refuses to delete files present on origin/main.

Verified: tsc clean, eslint clean, 1,420 tests across 175 files, production
build succeeds, zero deletions in the staged diff.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
dieterolson added a commit that referenced this pull request Aug 18, 2026
The last and hardest of the ten symbols blocking the React migration.
flight.ts gains AngularFlightPoint, FlightSimulationOptions and
simulateFlightWithOptions; FlightResult.trajectory widens to
AngularFlightPoint[]; and the in-file RK4 loop moves into a new
flightIntegrator.ts that both entry points delegate to, so there is one
integrator rather than two that can drift.

This was deferred twice because main's loop carries #4518's ground-crossing
guard and a naive swap would revert it. It does not: the integrator's
contact test requires currentGap > 0 strictly, so a launch starting at
ground level yields a gap of zero and never records a crossing --
structurally the same protection && t > dt provides.

That is not argued from the code alone. All 1,420 tests on main pass
unchanged with the integrator swapped in, including wind.test.ts and the
ball_flight_metrics_golden_v1 and inverse_flight_solver_golden_v1
Python-parity fixtures.

The integrator adds an upfront MAX_FLIGHT_INTEGRATION_STEPS = 50_000 bound
keeping synchronous UI-thread RK4 work finite; the default 10s at a 1ms
step is 10,000 steps, so no existing caller changes behaviour.

Unlocks flightGroundTransfer and simulationTypes, which need the angular
state at landing; their 15 tests land with them.

Verified: tsc clean, eslint clean, 1,435 tests across 176 files, production
build succeeds.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant