Skip to content

feat(swing-sim): land the ground skid/roll/bounce module as a standalone slice of #4466 - #4517

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

feat(swing-sim): land the ground skid/roll/bounce module as a standalone slice of #4466#4517
dieterolson merged 2 commits into
mainfrom
feat/swing-sim-ground-slice

Conversation

@dieterolson

Copy link
Copy Markdown
Collaborator

Lands swing_sim.ground (92 files) from consolidated/rate-closure-remainder-2026-08-13 as a self-contained slice against current main.

Why a slice and not a merge

#4466 cannot be merged. After #4473 squash-merged, the shared merge-base collapses to a commit where src/rate_of_closure/ did not exist, so every overlapping file is a both-added conflict with no common ancestor — 281 of them, zero trivially identical. Neither resolution direction produces a working tree:

strategy result
-X theirs (prefer main) 47 failed / 40 errors — net-new files call APIs main's versions lack
-X ours (prefer branch) 19 collection errors — breaks main's newer morris/variation work

The two snapshots have divergent, mutually-coupled API generations. The unique work is separable, so it lands feature-first.

Why ground is the right first slice

Its only dependencies outside the package — flight/result_metrics, solver/spatial_targets, solver/target_serialization — are already byte-identical on main. So this adds the module, swing_sim/canonical_numeric_json.py, and the ten shared Python/TypeScript ground golden fixtures, and changes nothing else.

Six broken tests repaired

All six fail on #4466's branch itself, verified unmerged. That PR has armed auto-merge and could never have gone green.

  • 4 x test_skid_roll_passivity built SurfaceRun without active_surface (added with regional-surface support). Every case raised TypeError in its fixture, so none had ever asserted a passivity property. Now passes request.surface, matching simulate_skid_roll.
  • test_bounce_cancellation_is_typed_and_retains_request_identity hardcoded a termination time and zero elapsed span that violate RepeatedBounceResult's chronology invariants. Both are now derived from the prefix.
  • ground/__init__.py eagerly imported to_ground_model_result, leaking the explicitly unqualified compatibility adapter onto the package namespace despite its deliberate absence from __all__ and the lazy-import map — precisely what test_unqualified_compatibility_adapter_is_not_public guards. Unused there; dropped.

Verification

  • 321 ground tests pass (was 46 failed / 14 errors before the fixture and test fixes)
  • ruff check, ruff format --check, mypy clean on the changed files (60 source files)
  • scripts/check_tools_manifest_layout.py passes
  • SPEC.md 1.17.13 row added

Part of epic #4103.

🤖 Generated with Claude Code

…one slice

`consolidated/rate-closure-remainder-2026-08-13` (#4466) cannot be merged: after
#4473 squash-merged, every overlapping file is a both-added conflict with no
common ancestor, and neither resolution direction yields a working tree
(`-X theirs` → 47 failed / 40 errors; `-X ours` → 19 collection errors). Its
unique work is separable, so it lands feature-first instead.

`swing_sim.ground` is the cleanest slice. Its three dependencies outside the
package — `flight/result_metrics`, `solver/spatial_targets`,
`solver/target_serialization` — are already byte-identical on `main`, so this
adds the 92-file module, `swing_sim/canonical_numeric_json.py`, and the ten
shared Python/TypeScript ground golden fixtures, and changes nothing else.

Six ground tests fail on the source branch itself, verified by running them
there unmerged — #4466 has armed auto-merge and could never have gone green:

- the four `test_skid_roll_passivity` cases build `SurfaceRun` without
  `active_surface`, which arrived with regional-surface support. Every one
  raised `TypeError` in its fixture, so none had ever asserted a passivity
  property. Fixed to pass `request.surface`, matching `simulate_skid_roll`.
- `test_bounce_cancellation_is_typed_and_retains_request_identity` hardcoded
  the request's separation time and a zero elapsed span. `RepeatedBounceResult`
  requires the termination to match the final trajectory point and the elapsed
  span to match bounce chronology, so it raised before reaching the assertions
  it exists for. Both are now derived from the prefix.
- `ground/__init__.py` eagerly imported `to_ground_model_result`, publishing the
  explicitly unqualified compatibility adapter on the package despite its
  deliberate absence from `__all__` and the lazy-import map — exactly what
  `test_unqualified_compatibility_adapter_is_not_public` guards. The name is
  unused inside `__init__.py`; the import is dropped.

321 ground tests pass. Scoped ruff, ruff format and mypy clean; the tools
manifest layout check passes.

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 05:12
dieterolson pushed a commit that referenced this pull request Aug 17, 2026
…e-base wall

Repo policy is to update the handoff with every PR, and two things changed
since #4514 landed.

The 39 codex/4142-* / codex/4433-* drafts are now closed as superseded by
merged #4473, which says so in its own description. Open PRs went 58 -> 21 and
drafts 39 -> 0. Their work is verifiably on main: the Morris chain as
application/morris/{host,client,contracts}.py, the #4433 chain as the flight,
putting, plot, visual-state and tab-audit modules. Their branches are left
intact, so the guidance is now reopen-rather-than-rebase — those branches have
diverged far enough that merging one would remove current main content.

#4466 turns out not to be mergeable at all. After #4473 squash-merged, the
shared merge-base collapses to a commit predating src/rate_of_closure/, making
every overlapping file a both-added conflict with no common ancestor (281 of
them). #4517 is landing it as standalone slices instead, starting with
swing_sim.ground. The other consolidations should expect the same wall, so the
roadmap now says slice rather than merge.

Also added a do-not against per-file Python version guards, pointing at the
conftest floor guard that replaces them.

Handoff remains within the 150-line policy at 144.

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

Two CI-only failures on the ground slice, neither reproducible on the local
Python 3.13 run that reported 321 passing.

`tests (3.11)` aborted during collection: `ModuleNotFoundError: No module named
'jsonschema'`. `test_profile_schema.py` and `test_schema_migration.py` validate
the ground profile schema with `Draft202012Validator`. It is a test-only
dependency — no `swing_sim` source imports it — and it happened to be installed
locally, which is why the gap only showed in CI. Declared in `requirements.txt`
rather than wrapped in `pytest.importorskip`: skipping would leave the profile
JSON Schema contract unguarded in CI, which is the opposite of what these tests
are for.

`quality-gate`'s Changed Test Assertion Check flagged
`ground/tests/_support.py` as a changed test file with no behavioral assertion.
That is correct and intended — it builds shared requests and results, and every
assertion about what it produces lives in the modules that consume it. Added to
`scripts/test_assertion_allowlist.txt`, the mechanism the check names in its own
failure message, next to the existing `swing_sim` entries.

Verified locally: `scripts/check_test_assertions.py` passes against this
branch's changed-file list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dieterolson
dieterolson merged commit f3d042c into main Aug 17, 2026
17 of 19 checks passed
dieterolson pushed a commit that referenced this pull request Aug 17, 2026
…e-base wall

Repo policy is to update the handoff with every PR, and two things changed
since #4514 landed.

The 39 codex/4142-* / codex/4433-* drafts are now closed as superseded by
merged #4473, which says so in its own description. Open PRs went 58 -> 21 and
drafts 39 -> 0. Their work is verifiably on main: the Morris chain as
application/morris/{host,client,contracts}.py, the #4433 chain as the flight,
putting, plot, visual-state and tab-audit modules. Their branches are left
intact, so the guidance is now reopen-rather-than-rebase — those branches have
diverged far enough that merging one would remove current main content.

#4466 turns out not to be mergeable at all. After #4473 squash-merged, the
shared merge-base collapses to a commit predating src/rate_of_closure/, making
every overlapping file a both-added conflict with no common ancestor (281 of
them). #4517 is landing it as standalone slices instead, starting with
swing_sim.ground. The other consolidations should expect the same wall, so the
roadmap now says slice rather than merge.

Also added a do-not against per-file Python version guards, pointing at the
conftest floor guard that replaces them.

Handoff remains within the 150-line policy at 144.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dieterolson pushed a commit that referenced this pull request Aug 17, 2026
…e-base wall

Repo policy is to update the handoff with every PR, and two things changed
since #4514 landed.

The 39 codex/4142-* / codex/4433-* drafts are now closed as superseded by
merged #4473, which says so in its own description. Open PRs went 58 -> 21 and
drafts 39 -> 0. Their work is verifiably on main: the Morris chain as
application/morris/{host,client,contracts}.py, the #4433 chain as the flight,
putting, plot, visual-state and tab-audit modules. Their branches are left
intact, so the guidance is now reopen-rather-than-rebase — those branches have
diverged far enough that merging one would remove current main content.

#4466 turns out not to be mergeable at all. After #4473 squash-merged, the
shared merge-base collapses to a commit predating src/rate_of_closure/, making
every overlapping file a both-added conflict with no common ancestor (281 of
them). #4517 is landing it as standalone slices instead, starting with
swing_sim.ground. The other consolidations should expect the same wall, so the
roadmap now says slice rather than merge.

Also added a do-not against per-file Python version guards, pointing at the
conftest floor guard that replaces them.

Handoff remains within the 150-line policy at 144.

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

Copy link
Copy Markdown
Contributor

⚠️ SPEC.md Update Required

Source files were modified in this PR but SPEC.md was not updated.

Per the Specification-Driven Development Policy:

  • Any PR that adds, removes, or changes functionality must update SPEC.md
  • Update the relevant sections (features, architecture, tests, dependencies, etc.)
  • Bump the Spec Version if making substantive changes

To resolve:

  1. Update SPEC.md to reflect your changes, OR
  2. Add the spec-exempt label if this PR genuinely doesn't affect the spec (e.g., pure refactor with no behavior change)

This check is enforced by the spec-check CI workflow.

dieterolson added a commit that referenced this pull request Aug 17, 2026
… at It (#4516)

* fix(ci, tests): enforce the two-tier Python floor instead of guessing at it

The `tests (3.10)` lane was running code that requires 3.11, which produced
failures that looked like defects and were not.

This repo is deliberately two-tier. The root distribution declares
requires-python = ">=3.11" (classifiers 3.11/3.12, mypy python_version 3.11),
while ten sub-packages and Rust crates declare >=3.10 and ship 3.10 wheels from
six maturin workflows. The 3.10 matrix lane exists to validate those
sub-packages. But it ran the whole suite, including root-package code such as
src/p1am_control_system and src/shared/python.

Two failures came from exactly that mismatch:

- test_deployment_hardening.py imported tomllib, stdlib only on 3.11+, aborting
  collection for the entire session (1,218 collected, zero executed).
- test_estop_shutdown_safe_state.py timed out because asyncio.wait_for was
  reimplemented on asyncio.timeouts in 3.11 with different cancellation
  propagation.

Neither is a 3.10 support bug. Both are 3.11-only code being run on 3.10.

conftest.py now resolves the nearest pyproject.toml for any collection path,
reads its declared requires-python floor, and skips collection when the running
interpreter is below it. The floor is read from the declarations rather than
hardcoded, so adding a sub-package or moving a floor needs no edit here.
requires-python is regex-parsed on purpose: tomllib does not exist on the very
interpreter the guard has to run on. It fails closed at (3, 11) when nothing
parses. Verified as a strict no-op on 3.11+ — collection is identical at
10,382/10,414 with and without the guard.

CLAUDE.md advertised a flat "Python 3.10+" that the root distribution rejects.
It now states the real contract, names the ten 3.10 distributions, explains
what the 3.10 lane is for, and says not to add per-file version guards because
the conftest handles it.

tests/test_python_version_contract.py locks the declarations together:
requires-python against the mypy target and the classifiers, the CI matrix
against the lowest declared floor, CLAUDE.md against the root floor, and a
regression guard proving root-package tests are excluded below the floor while
3.10 sub-packages are still collected. The interpreter version is faked so
those guards run on every lane, not only the one that cannot execute them.

This supersedes the separate investigation into the e-stop shutdown timeout:
that test should not run on 3.10 at all, and no shutdown-safety code needed to
change.

The pytest.importorskip added to test_deployment_hardening.py in #4515 is now
redundant but retained deliberately as defense in depth — if this guard ever
regresses, a bare import there takes down the whole session again.

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

* docs(agent-handoff): record the closed draft chain and the #4466 merge-base wall

Repo policy is to update the handoff with every PR, and two things changed
since #4514 landed.

The 39 codex/4142-* / codex/4433-* drafts are now closed as superseded by
merged #4473, which says so in its own description. Open PRs went 58 -> 21 and
drafts 39 -> 0. Their work is verifiably on main: the Morris chain as
application/morris/{host,client,contracts}.py, the #4433 chain as the flight,
putting, plot, visual-state and tab-audit modules. Their branches are left
intact, so the guidance is now reopen-rather-than-rebase — those branches have
diverged far enough that merging one would remove current main content.

#4466 turns out not to be mergeable at all. After #4473 squash-merged, the
shared merge-base collapses to a commit predating src/rate_of_closure/, making
every overlapping file a both-added conflict with no common ancestor (281 of
them). #4517 is landing it as standalone slices instead, starting with
swing_sim.ground. The other consolidations should expect the same wall, so the
roadmap now says slice rather than merge.

Also added a do-not against per-file Python version guards, pointing at the
conftest floor guard that replaces them.

Handoff remains within the 150-line policy at 144.

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

* fix(tests): check each sub-package at its own floor, not the global minimum

The 3.11 lane failed on the previous commit with "src/movement_optimizer
declares 3.10 support but the guard would exclude it from the 3.10 lane". The
guard was not the problem; the test was wrong by construction.

test_sub_package_tests_still_run_on_the_lower_lane faked the interpreter to the
*minimum* floor across every lower-floored package, then asserted that every one
of them would still be collected. That only holds when they all declare the same
floor. A single package declaring lower than the rest makes the assertion fail
for packages that are being excluded entirely correctly — a 3.10 package is
supposed to be excluded on 3.9.

Each package is now checked at its own declared floor, which is the property
actually worth guarding and is independent of what anything else declares.

Two related changes:

_below_declared_floor now takes the interpreter version as an optional
parameter instead of the tests patching sys.version_info. That patch was
process-global and read by unrelated library code, which under xdist meant a
test could perturb workers running beside it.

test_conftest_reads_each_package_declared_floor now checks every sub-package
rather than two hand-picked ones, and reports all mismatches with their paths.
Path resolution is the part most likely to differ across platforms, so a failure
should name the package instead of surfacing as a confusing assertion further
downstream.

Also added test_nested_distributions_do_not_widen_their_parent_tree. The
diagnostic turned up a real wrinkle: psa_package declares >=3.10 while living
inside src/shared/python, which is root-package code. The guard handles it
correctly — the nested distribution gets 3.10 and its parent tree stays at
3.11 — but nothing was pinning that, and a regression there would quietly let
the lower lane start collecting root code again.

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

* ci: allow empty collection on lanes below the root Python floor

The floor guard works, and that turned out to be the problem for this PR's own
3.10 lane: pytest exited 5, "no tests collected".

On a sub-floor lane the guard excludes root-package code, including the
top-level tests/ tree. A change that touches only root-package tests therefore
has nothing to run there, and pytest treats an empty selection as an error. The
lane failed while behaving exactly as designed.

The tests step now tolerates exit code 5, but only on lanes below the root
requires-python floor, which is read from pyproject.toml rather than hardcoded
so it tracks the declaration. On the required lane exit 5 still fails, which is
what keeps the issue #3324 vacuous-selection protection meaningful — that guard
exists to catch typo'd or broken test selection, and blanket-tolerating exit 5
everywhere would defeat it.

Verified before pushing, since this is a CI workflow change and the previous two
rounds of surprises here were self-inflicted:

- version comparison: 3.9 and 3.10 allow empty collection, 3.11 and 3.12 do not
- tolerance: exit 5 returns 0 when allowed, and passes through unchanged when not
- the workflow YAML parses and the extracted run block passes bash -n

Deliberately avoided a python heredoc inside the indented YAML block; the floor
is parsed with grep instead, because heredoc terminators in a YAML block scalar
are sensitive to how the block is de-indented.

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

* ci: drop the 3.10 lane from ci-standard instead of propping it up

Replaces the exit-5 tolerance from the previous commit, which was treating a
symptom.

Two facts settle it. Every core_tests entry is root-package code — all of them
are tests/** or src/shared/python/sidekick/tests/** — so once the floor guard
correctly excludes root-package code, the 3.10 lane can never run a single core
test. And 3.10 compatibility for the ten sub-packages that actually claim it is
already proven elsewhere: each crate's maturin-*.yml runs a build + parity gate
across 3.10/3.11/3.12 that builds the wheel, installs it, and asserts both that
the extension imports and that the native backend is selected.

So the lane was not testing 3.10 support. It was running root-package code on an
interpreter that cannot execute it, and the only reason it ever looked
meaningful was that a collection abort masked the emptiness. Tolerating empty
collection would have institutionalised a lane that consumes a runner and
reports nothing on every PR.

ci-standard now runs ["3.11", "3.12"], starting at the root floor. The exit-5
tolerance is reverted, and the now-dead `!= "3.10"` sidekick guard is removed.

Two contract tests replace the weaker matrix check:

test_ci_matrix_starts_at_the_root_floor fails if a lane is ever added below the
root floor, with an error that points at the maturin workflows as the correct
home for lower interpreters.

test_lower_floor_packages_keep_a_workflow_that_exercises_them fails if a
sub-package declares a floor below the root and no workflow outside ci-standard
actually runs that interpreter. This is the guard against the real risk in this
change: dropping the lane must not leave a 3.10 claim with nothing behind it.
It passes today, which is what makes the removal safe rather than assumed.

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

* fix(tests): select sub-package pyprojects by git tracking, not a denylist

The 3.11 lane failed on a vendored dependency:

  .cargo-home/registry/src/.../pyo3-0.24.2/pyo3-runtime/pyproject.toml
  declares >=3.7 but no workflow outside ci-standard runs that interpreter

That file is pyo3's own packaging metadata sitting in the cargo registry cache.
It is a third-party artifact, not a claim this repository makes, and the
directory does not exist locally — it is materialised only on CI runners, which
is why the test passed here and failed there.

The helper was enumerating pyproject.toml by rglob and filtering with a
directory denylist (.venv, node_modules, target, build, dist, .git). A denylist
is unbounded: every new tool cache is another entry nobody adds until it breaks
a build. .cargo-home was simply the first one to appear.

Selection is now by `git ls-files`, which answers "is this ours?" directly and
cannot drift as new caches appear. It returns exactly the eleven real
sub-package pyprojects and nothing else. If git is unavailable the tests skip
rather than silently degrading to a weaker check.

This fixes all four tests that share the helper, not just the one that failed.

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

---------

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
)

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 17, 2026
…main (#4523)

`rate_of_closure.variation.regional_ground_study_adapter` cannot be imported on
current `main`:

    ImportError: cannot import name 'to_ground_model_result'
    from 'shared.python.swing_sim.ground'

Two of my own PRs combined to cause it. #4517 removed that name from the ground
package's eager exports, correctly — the package's own
`test_unqualified_compatibility_adapter_is_not_public` requires the explicitly
*unqualified* compatibility adapter to stay private, and it is absent from both
`__all__` and the lazy-import map. #4520 then landed a consumer importing it
from the package path. Neither PR's tests imported the adapter module, so both
were green while three modules could not be loaded at all:
`regional_ground_study_adapter`, `regional_ground_variation`, and
`regional_ground_target_projection`.

The import now names the owning module,
`shared.python.swing_sim.ground.result_adapter`, which does export it. That
keeps the package contract intact — the unqualified adapter is still not
package-public — while making the unqualified dependency explicit where it is
used, which is the honest thing for a compatibility path.

Adds `tests/rate_of_closure/test_variation_module_importability.py`, which
imports every module in the package. This is the check that was missing: a
suite can be entirely green while a module in it is unreachable, and no
existing gate covers that. It also catches the neighbouring cases — a symbol
dropped from a package's `__all__`, a rename, a new circular import. Verified
by reverting the one-line fix: 3 failed, 35 passed.

The ground contract test still passes, so this does not weaken what #4517
established. 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>
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