Skip to content

Rate of Closure remainder: club builder, impact tensor, flight, multi-view workspace, ground and web companion (consolidates 43 PRs) - #4466

Open
dieterolson wants to merge 619 commits into
mainfrom
consolidated/rate-closure-remainder-2026-08-13
Open

Rate of Closure remainder: club builder, impact tensor, flight, multi-view workspace, ground and web companion (consolidates 43 PRs)#4466
dieterolson wants to merge 619 commits into
mainfrom
consolidated/rate-closure-remainder-2026-08-13

Conversation

@dieterolson

Copy link
Copy Markdown
Collaborator

Consolidates the remaining 43 open src/rate_of_closure PRs into one branch. They all
edit the same tree, so separate PRs would conflict with one another and multiply CI by
~43x. Every stack was folded top-down (each child into its own parent, then the parent
upward) so no parent is left a slice behind, and the two non-tip carriers the lead flagged
are explicitly included: #4171 (8067ca79a CAD export artifact validation, absent from
tip #4172) and #4309 (f9afa9d96 strict ground result playback, absent from tip #4312).

bash verify_coverage.sh consolidated/rate-closure-remainder-2026-08-13 <all 43> reports
safe=43 NOT-covered=0 — closing every superseded PR drops nothing.

What this carries

Family PRs
Club builder / impact tensor #4119, #4170, #4171, #4172, #4326, #4329, #4334, #4338, #4341
Flight / wind / wedge #4190, #4204, #4207, #4208, #4210, #4211, #4213, #4214, #4215, #4216
Multi-view workspace #4301, #4303, #4324, #4327, #4330, #4331, #4333, #4336, #4340, #4343, #4344, #4348, #4349
Ground #4299, #4305, #4306, #4307, #4308, #4309, #4312, #4328
Web companion #4390, #4391, #4392

Correctness findings fixed here (not merge mechanics)

These are defects the merge exposed. Each was measured, not assumed.

  1. Two unguarded from datetime import UTC imports (rate_of_closure/ui/pyqt6/torque_profile_controller.py,
    tests/unit/sidekick/agent/test_action_audit.py) arrived with feat: Swing–Impact–Ball-Flight Simulation Platform (consolidated — epic #4103) #4119's chain. datetime.UTC
    does not exist on Python 3.10, which the test matrix still runs; main has only
    version-gated imports. Both reverted to timezone.utc, and the # noqa: UP017 is
    re-anchored onto the physical line that uses it so a format pass cannot displace it again.
  2. The PyQt main window could not construct at all (KeyError: 'regional_surfaces', 78
    tests). The workspace-file family hard-coded a 10-entry PyQt-to-canonical tab map while
    the ground families added three tabs. CANONICAL_MODULE_IDS now matches the React
    client's PRIMARY_VIEWS exactly (12 shared modules); the PyQt-only Ground Study tab is
    declared unshared, excluded from the cross-client document, and restored in place on
    import rather than dropped. Python demanded 10 module ids while React validated 12 — the
    two clients disagreed, so no workspace file could round-trip between them.
  3. The multi-view compositor can lay a hosted view out at zero height (measured 88x0).
    Matplotlib then built a degenerate axes transform and raised
    LinAlgError: Singular matrix from axvline. The shared canvas now keeps the figure at
    least one pixel, which protects every plot in the compositor rather than one call site.
  4. feat(rate-of-closure): bind club assembly to simulation #4341's club-assembly binding was not wired through the simulation on either client.
    Restored end to end: SimulationConfig validates a qualified binding, SimulationRun
    carries club_assembly_usage, the pipeline feeds the bound head mass and inertia tensor
    to the impact solver and reports not_used on a miss, ManualSwingSource declares its
    selected-head attitude, and the TypeScript twin matches.
  5. The club STL / engineering-sidecar / assembly-binding controls were missing from the
    PyQt ControlsPanel and the rate_of_closure.club facade, so feat(rate_of_closure): export selected clubhead STL #4326-feat(rate-of-closure): bind club assembly to simulation #4341's exports were
    unreachable (club_artifact_ui imported six names the package no longer exposed).
  6. rolling_projection projected onto the request surface, not the active one — after a
    region transition the no-slip projection used the surface the ball had left.
  7. filelock is imported by 15 always-collected test modules but was declared only in an
    optional extra
    , so tests (3.11) would have failed at collection. Declared in
    requirements.txt, which is what CI installs from.
  8. Two derived release artifacts drifted from the unioned campaign manifest: the
    four-surface capability manifest was missing the five linked ground specifications
    (now 24 active specs / 39 declarations), and carriers recorded a self-referential
    head_sha where docs/release's own contract test requires the immutable
    evidence_commit_sha.
  9. The React client did not compile. Type-check, ESLint, vitest and the production build
    all pass now; the ground-playback comparison overlay and event markers, the chip
    forgiveness objective controls, and the tracking-aware camera were reconciled across
    families. The toolstrip had two anonymous role="status" live regions; both now have
    distinct accessible names.
  10. 90 of 103 open PRs carried six unreachable .codex-worktrees gitlinks pointing at
    commits that exist on no remote. Removed from the index (git ls-files -s | grep ^160000
    is empty). The .gitignore line landed with CONS-A1.

Structural finding worth recording

#4119 shares no development history with the other 42 PRs beyond the 2026-08-06 base: it
is a parallel, pre-refactor implementation of the same subsystem (monolithic simulation_tab,
kinetics, plotting/catalog, App.tsx), whereas the trunk had since split those modules.
Its conflicts were therefore resolved toward the trunk's later structure, with #4119's unique
deliverables (the club-assembly/STL slices above) ported forward. The same applies at smaller
scale to the flight family (#4207/#4210 chains), whose files the trunk had already refactored.
Ground playback was implemented twice in parallel (#4390 line and #4305 chain); the
app-wired implementation is kept and the #4305 chain's comparison capability was ported into
it rather than discarded.

Verification performed locally

quality-gate was reproduced with its exact pins, and every blocking step passes:

  • ruff 0.14.10 (CI's pin, not a newer local build) check + format --check over all 770
    changed non-excluded Python files: clean. 98 modules arrived formatted by newer ruff builds
    and were normalised — that mismatch is what turned 🎨 Palette & Bolt Suite: Form Submission, Accessibility, SVG & CSV Optimizations #4429's gate red.
  • mypy 1.13.0, MYPYPATH='src;src/python/src' (semicolon: the : in CI's Linux export
    parses as one nonexistent directory on Windows and makes mypy pass having checked nothing),
    one invocation over all 545 changed non-test modules via @listfile, numpy <2.4:
    137 errors -> Success.
  • Reject Raw Merge Conflict Markers, Minimum Test Contract, Changed Test Assertion Check,
    bandit (-ll -ii, 495 files), and the Module Size Budget step inside quality-gate
    (a dead duplicate helper had pushed motion_tabs.py to 1203 lines): all pass.
  • tests/rate_of_closure: 1520 passed. The 30 remaining local failures are all
    ModuleNotFoundError: ntsecuritycon — pywin32 is absent from this box and the code path is
    os.name == "nt"-guarded, so Linux CI skips it.
  • React: tsc --noEmit clean, ESLint zero warnings, 1097 vitest tests pass, production
    build succeeds.

Not included

The four PRs d-sorgcodexagent[bot] opened during this work (#4451, #4452, #4453, #4465) are
deliberately left out: they are stacked on codex/4433-club-explorer-camera, which is
#4444's head inside the Morris/variation consolidation (#4447), not on any of these five
families. Absorbing them would pull that entire 26-PR chain into this branch and duplicate
content that #4447 already carries. They belong on top of #4447.

Closes #4111
Closes #4136
Closes #4143
Closes #4144
Closes #4148
Closes #4149
Closes #4150
Closes #4159
Closes #4160
Closes #4163
Closes #4164
Closes #4189
Closes #4192
Closes #4193
Closes #4194
Closes #4195
Closes #4196
Closes #4197
Closes #4198
Closes #4199
Closes #4200
Closes #4224
Closes #4225
Closes #4261
Closes #4264
Closes #4272
Closes #4273
Closes #4274
Closes #4275
Closes #4284
Closes #4300
Closes #4379
Closes #4380
Closes #4385

Part of #4103
Part of #4104
Part of #4105
Part of #4106
Part of #4107
Part of #4108
Part of #4109
Part of #4110
Part of #4120
Part of #4125
Part of #4146
Part of #4158
Part of #4180
Part of #4191
Part of #4205
Part of #4218
Part of #4220
Part of #4260
Part of #4267
Part of #4377

Supersedes #4119, #4170, #4171, #4172, #4190, #4204, #4207, #4208, #4210, #4211, #4213,
#4214, #4215, #4216, #4299, #4301, #4303, #4305, #4306, #4307, #4308, #4309, #4312, #4324,
#4326, #4327, #4328, #4329, #4330, #4331, #4333, #4334, #4336, #4338, #4340, #4341, #4343,
#4344, #4348, #4349, #4390, #4391, #4392

codex-scheduled added 30 commits August 11, 2026 01:36
…rors

My first local run passed because the verification venv had no PyQt6 or
matplotlib, so mypy resolved those imports to Any under
--ignore-missing-imports. CI has them installed and infers precise types, which
inverted several diagnostics. The venv now carries CI's dependency set, and the
local run reproduces CI exactly (23 -> 0).

* Casts around Qt getters are replaced by annotated locals, which are correct
  whether the stubs return Any or a precise type, so this cannot flip again.
* Keyword-splat dicts (course_scene style, plot_wizard common) are declared
  dict[str, Any] instead of being inferred as dict[str, object].
* One shared attribute had two incompatible declarations after the mixin union:
  the file-commands mixin declared _navigation_settings as QSettings while the
  navigation mixin declared the NavigationSettings protocol. Both mixins, the
  compositor and SimulationTab now use the protocol, which is all any of them
  needs (value/setValue) and which QSettings satisfies structurally.

144 PyQt tests over the affected surfaces pass.
@dieterolson
dieterolson disabled auto-merge August 14, 2026 10:11
@dieterolson
dieterolson enabled auto-merge (squash) August 14, 2026 10:11
codex-scheduled added 5 commits August 14, 2026 12:59
…solidation

Three conflicts, all in the Bolt/Palette surface #4438 just landed:

* motion_tabs.py — the two branches extracted *different* things from the same
  module: #4438 pulled the view code into motion_view.MotionViewMixin, while
  this branch pulled the palette and geometry helpers into motion_helpers.
  Kept both. The file is now based on main's post-extraction version, imports
  build_motion_colors/chain_path_length from motion_helpers instead of
  redefining them inline, and drops the rendering import that became unused.
  1093 -> 1074 lines, so no duplicated helper and no module-budget pressure.
* torque_utils.py and test_sidekick_ux_hardening.py — resolved to main's
  versions. The two torque implementations are behaviourally identical (both
  evaluate ascending coefficients; the merged pendulum test pins 2 + 3t), so
  main's Bolt form wins rather than re-imposing this branch's TorquePolynomial
  call. shared/python/swing_sim/torque_profiles still ships and is still
  consumed by the rate-of-closure torque workspace features.

test_action_audit keeps timezone.utc with the noqa anchored on its own line, and
the anchored grep for a top-level datetime UTC import is empty.
rate_of_closure.web_companion.runtime and web_authority.child both do a plain
module-level 'import uvicorn', and tests/rate_of_closure imports them, so the
whole pytest lane aborts at collection without it. uvicorn was declared only in
pyproject's optional rate-of-closure-web extra, which CI does not install; CI
installs from requirements.txt. Same class as the filelock gap, and the same
failure that just took #4447 red.

Found by walking the import graph from tests/ through first-party modules and
diffing the unguarded external roots against requirements.txt plus the explicit
pip lines in ci-standard.yml. The only other new-in-this-branch hits were
starlette (a fastapi dependency CI installs) and mpl_toolkits (part of
matplotlib), both already satisfied.
…e SPEC row

The doc-union strategy that kept every family's SPEC and handoff content also
concatenated every family's handoff narrative, leaving AGENT_HANDOFF.md at 4872
lines and src/rate_of_closure/AGENT_HANDOFF.md at 5091 — the exact opposite of
the documented contract for those two files (current-state only, <=150 lines,
history in git). Rewritten from the consolidated tree's real state at 85 and 101
lines, keeping what a next agent actually needs: the cross-client module-registry
contract, the regional-ground plan ownership, the zero-size-canvas rule, the
derived release-evidence chain, and the packaging rule that a module-level import
in a collected test must be declared in requirements.txt.

docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md is deliberately left long:
it is a campaign narrative referenced as evidence by the campaign manifest, not
one of the two policy-governed handoff files.

Adds the dated SPEC 1.14.96 change-log row this consolidation owes under the
spec-check contract, and moves Last Spec Update to 2026-08-14 (it still read
2026-08-12 while newer rows had landed).
… contract test

Merge conflicts:

* .github/workflows/cross-repo-python-integration.yml and its contract test —
  resolved to main's versions, deliberately NOT unioned. #4480 narrowed the
  UpstreamDrift sparse scope to `src/shared` and deleted the pinned-submodule
  step, with a comment explaining that UpstreamDrift moved its consumed packages
  under src/shared/python and that `pip install -e .` resolves through
  hatchling's `packages = ["src"]`. Its test asserts set *equality*
  (`actual == REQUIRED_SPARSE_PATHS`), so re-adding this branch's wider scope or
  the submodule-init step would fail the very test #4480 fixed. The wide scope is
  the superseded side, not content worth preserving; nothing else references the
  removed step. The workflow is now byte-identical to main, keeping #4475's
  CI_RUNNER_MODE fallback and public-repo hosted routing.
* AGENT_HANDOFF.md — kept this branch's policy-compliant rewrite and folded in
  main's new fact (#4406 drift-transfer diagnostics under UpstreamDrift epic
  #8551), 89 lines.
* SPEC.md — unioned, newest first.

canonical numeric JSON: the guard and the test contradicted each other and both
arrived from this branch's own fold. Resolved in favour of the guard, and the
test rewritten to assert the narrowed contract.

Evidence that the strict default is the intended contract, not an accident:

* The guard appears in three coordinated places — the float path, the integer
  path, and a named opt-out (`canonical_numeric_json_extended_floats`) whose
  docstring says "beyond the safe range while retaining safe integers".
* A dedicated facade, src/rate_of_closure/variation/canonical_numeric_json.py,
  re-exports the extended encoder under the plain name for the one caller that
  needs it (capability-observation payloads carrying declared 1e20/1e21
  magnitudes). That is how a contract gets narrowed while an established caller
  keeps working.
* The strict encoder's only production consumer is
  regional_ground_authority_status.py — the browser-facing authority job-status
  wire, where a value above 2**53-1 would reach the browser as a different
  number.
* The old test's own second case already asserted the +/-(2**53-1) boundary,
  i.e. the file half-agreed with the guard. The failing case was a pre-guard
  regression guard ("preserves established... domain") that the narrowing
  superseded.

The default guard is untouched: loosening it would make every cross-runtime
payload silently accept values JavaScript cannot represent, which is a contract
that cannot fail. The test now asserts (a) the default path fails closed for
out-of-range floats, (b) the opt-in path still serializes that domain exactly
with no exponent notation, and (c) the opt-in path still refuses unsafe
integers. canonical_numeric_float stays strict: its production consumers
(ground contract_types, ground_playback_comparison) carry physical magnitudes
far below the limit, so an unused opt-out would be speculative.

Not fixed here: test_wind.py's abs=1e-12 cross-client tolerance is #4482's
(~1600 ULP of accumulation under a different libm, not one rounding).
dieterolson added a commit that referenced this pull request Aug 17, 2026
…ate (#4514)

Both documents had drifted far enough to actively misdirect the next agent,
and the root document had also outgrown its own policy by 18x.

CLAUDE.md requires handoff docs to be current-state only and at most 150
lines, with history in git. The root doc had instead accumulated 137 dated
entries across 2,708 lines. Those entries are moved verbatim to
docs/agent_handoff_archive/2026-08_tools_root_handoff_log.md, not deleted —
the archived body is byte-identical to the original (165,356 chars, 137
entries), so no working-tree context is lost. The live doc is now 132 lines
and carries an explicit do-not against appending dated entries again.

Content corrections. The root doc presented PR #4119 as open with auto-merge
armed and #4124/#4129 as open drafts stacked on it; in fact #4119 closed
without merging and #4124/#4129 both merged. Epics #4142 and #4433 were absent
entirely. Delivery has shifted from stacked PRs to scoped consolidations
rebuilt onto current main.

It now records the seven live epics, the four open consolidations (#4446,
#4447, #4449, #4466) as the real queue, and the fact that 39 of 58 open PRs
are codex/4142-* / codex/4433-* drafts superseded by merged #4473 — whose own
description says so. Those should be triaged and closed, not rebased.

The pendulum doc described issue #4406 as active on branch
research/shoulder-velocity-drift-transfer. #4406 is closed and shipped via
consolidation #4450. It now records what landed, keeps the fail-closed
triple/golfer tier boundary as an explicit do-not, and points at the
UpstreamDrift #8684 qualification state — including the finite-ground result
where the preregistered screen admitted 0 of 384 cells, so a moving base is
not a free upgrade for the higher tiers.

Also recorded: the four pre-existing ruff format failures under
src/data_processing/ that are on main and should not be absorbed into an
unrelated PR, and the --regenerate-api-baseline trap.

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
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>
dieterolson added a commit that referenced this pull request Aug 17, 2026
…one slice of #4466 (#4517)

* feat(swing-sim): land the ground skid/roll/bounce module as a standalone 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>

* fix(swing-sim): declare jsonschema and allowlist the ground test support 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>

---------

Co-authored-by: codex-scheduled <codex-scheduled@users.noreply.github.com>
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>
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 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
… counterpart (#4518)

* feat(swing-sim): land the flight slice with its React parity counterpart

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>

* fix(swing-sim): satisfy the changed-file mypy gate in the ground pipeline

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>

* fix(swing-sim): guard the tee parity test and allowlist the pipeline 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>

---------

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
…olicy (#4521)

`CLAUDE.md` caps handoff documents at 150 lines and keeps history in git.
`src/rate_of_closure/AGENT_HANDOFF.md` had grown to 2,205 lines across 140
dated entries — the same append-only drift the root handoff already recorded
and corrected for itself, where its own do-not list now reads "It grew to 2,708
lines that way, 18x over the policy limit."

The 140 entries move verbatim to
`docs/agent_handoff_archive/2026-08_rate_of_closure_handoff_log.md`, following
the existing root-log archive convention and header, so no provenance is lost.

The live document is rewritten as 103 lines of current state, weighted toward
what actually cost time to rediscover:

- the PyQt6 entry point and window title the UpstreamDrift launcher manifest
  depends on, and where the React mirror and each `swing_sim` package live;
- why PR #4466 cannot be merged by any strategy, with the measurement rather
  than the assertion: `-X theirs` yields 47 failures and 40 errors, `-X ours`
  yields 19 collection errors, because the post-squash merge-base predates
  `src/rate_of_closure/` and every overlapping file is a both-added conflict;
- the four files where the source branch is *older* than `main`, each of which
  would have silently reverted shipped work if taken wholesale;
- what remains of #4466 by area, including that `application` and
  `web_authority` import each other and must land together;
- local-environment traps: mypy 1.13 crashing on Python 3.13 for multi-file
  sets, the two-tier `tools_core` capability, and PowerShell rewrites
  normalising `SPEC.md` to CRLF and turning the next merge into a whole-file
  conflict.

No source or test behaviour changes. SPEC 1.17.18.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment