Ground Study + Rate of Closure Suites: regional execution, camera tracking, job workspaces & static web distribution (consolidates 32 closed PRs, supersedes #4409/#4410) - #4446
Conversation
# Conflicts: # SPEC.md
…8-toolstrip-workspace # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…8-toolstrip-workspace # Conflicts: # SPEC.md
…4-variation-export-continuation # Conflicts: # src/rate_of_closure/AGENT_HANDOFF.md
…9-wind-scalar-adapter # Conflicts: # src/rate_of_closure/AGENT_HANDOFF.md
…9-wind-workflow # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…8-ground-contract # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…9-flight-ground-transfer # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md # src/shared/python/swing_sim/flight/tests/test_contract_api.py
# Conflicts: # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md
# Conflicts: # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md
# Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md # tests/rate_of_closure/test_python_compatibility.py
# Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…ated/ground-study-batch # Conflicts: # SPEC.md
…study-batch # Conflicts: # SPEC.md
…ground-study-batch # Conflicts: # SPEC.md
…idated/ground-study-batch # Conflicts: # SPEC.md
…-of-closure-batch # Conflicts: # SPEC.md # src/shared/python/golf_club/__init__.py # tests/ops/test_cross_repo_python_integration.py # tests/shared/python/golf_club/test_contracts.py
…-closure-batch # Conflicts: # SPEC.md
…e-of-closure-batch # Conflicts: # SPEC.md
…losure-batch # Conflicts: # SPEC.md
…ated/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…d/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…lidated/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…ted/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…ed/rate-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md # docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md # src/rate_of_closure/AGENT_HANDOFF.md
…e-of-closure-batch # Conflicts: # AGENT_HANDOFF.md # SPEC.md
…baseline, and fix lint
…eline, and fix lint
…sure-batch (#4410) Both branches consolidated the same already-closed #4332-#4402 range and shared ~360 commits while diverging by 49, so each was re-running the full CI matrix for largely identical content. This merge produces one carrier for both suites. Conflict resolution (5 files): * SPEC.md — the Identity version rows and the dated Change Log entries had accumulated NESTED conflict markers across ~25 successive feature-branch merges, and prior "resolutions" deleted markers arbitrarily, leaving the file UNBALANCED (73 open vs 104 close markers, with 14 orphaned `=======` and 31 orphaned `>>>>>>>` lines). Every side was an independent prepended changelog entry, so the resolution keeps the UNION of all sides; exact-duplicate blocks are collapsed. Verified line-for-line that no original content was dropped. The single-valued Identity rows are collapsed from 33/33/6 duplicates to one each, at the highest observed version (1.14.94) plus a consolidation bump. * .secrets.baseline — regenerated with the exact CI invocation. The inherited #4410 baseline had been regenerated on Windows and committed with BACKSLASH path keys, which no Linux CI scan can match; keys are restored to POSIX form. The result is a superset of both main's and #4409's audited entries and is idempotent under rescan. * AGENT_HANDOFF.md, docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md, src/rate_of_closure/AGENT_HANDOFF.md — balanced but committed conflict markers, resolved as the union of both branches' handoff entries. `git grep "<<<<<<< HEAD"` is now empty, so the quality-gate "Reject Raw Merge Conflict Markers" step that blocked #4409 and #4410 passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither of these belongs in the product tree, and neither exists on `main` -- both were committed by accident during the prior consolidation attempts. * `.codex-worktrees/*` — six mode-160000 gitlinks pointing at stale local Codex worktrees from June (PRs #3602-#3784, long since merged). There is no `.gitmodules`, so they are unregistered submodule entries. This is exactly the "unregistered historical gitlink" that the campaign handoff records as having broken `actions/checkout`'s cleanup step and forced a credential-free manual fetch workaround in the distribution workflow. * `batch_plan.txt`, `non_draft_prs.json`, `pr_worker_final_results.json`, `specified_summary.txt` — raw `gh` API dumps and PR-batching notes written by the prior consolidation agent into the repository root. They are CRLF-encoded and trip `git diff --check`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…icting rewrite The consolidation branches were formatted with a ruff other than the version `quality-gate` pins (`ruff==0.14.10`), which silently reformatted 100 changed Python files into output that the pinned formatter rejects. `main`'s versions of all 91 of those files that exist there pass `ruff format --check` under 0.14.10 cleanly, so this was pure version-drift churn introduced by the consolidation, not pre-existing debt -- and it is why Format Check was the next failure waiting behind the conflict markers. Re-running `ruff format` under the pinned 0.14.10 restores 85 of those 91 files to byte-identical-to-`main` content; the other 6 keep their genuine changes (for example the rotation-converter plot-helper extraction) and now differ from `main` only by real work. Separately reverts `tests/unit/sidekick/agent/test_action_audit.py`, where an automated UP017 fix rewrote `tzinfo=timezone.utc` to `tzinfo=UTC` and imported `UTC` from `datetime` while LEAVING IN PLACE the very suppression forbidding it: `# noqa: UP017 - Python 3.10 CI lacks datetime.UTC.` The `tests` matrix still includes Python 3.10, where `datetime.UTC` does not exist, so that rewrite would have failed the 3.10 job at import. Verified with the pinned toolchain over the exact gate file list (`git diff --diff-filter=ACMRT --name-only $(git merge-base origin/main HEAD) HEAD -- '*.py'` minus the gate's excluded prefixes, 731 files): ruff check clean, ruff format --check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9f01d99c1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| assert scenario.velocity_at( | ||
| case["time_s"], case["position_m"] | ||
| ) == pytest.approx(case["expected_velocity_mps"], abs=1e-12) |
There was a problem hiding this comment.
Relax the wind golden tolerance
On Python 3.12.13 with pytest 9.0.3, the shear_turbulence case fails here: Python returns 9.786440272809793, while the cross-client fixture expects 9.7864402728063, a normal 3.49e-12 cross-runtime sin/libm difference that exceeds this 1e-12 tolerance. The changed-test lane in .github/workflows/ci-standard.yml collects this src/**/tests/test_*.py file and runs it in the Python matrix, so the commit cannot pass that lane unless the comparison allows the observed floating-point drift.
Useful? React with 👍 / 👎.
| debug_assert!( | ||
| dt.is_finite() && dt > 0.0, | ||
| "rk4_step: dt must be finite and > 0" | ||
| ); |
There was a problem hiding this comment.
Validate timesteps in release builds
When the release-built PyO3 or WASM step API receives dt <= 0 or a non-finite timestep, this debug_assert! is compiled out, so zero silently returns an unchanged state, negative values integrate backward, and NaN propagates through the result instead of returning the advertised error. simulate calls the same function repeatedly, and the new workflow explicitly builds the wheel with --release; make this a runtime precondition that returns Err rather than a debug-only invariant.
AGENTS.md reference: AGENTS.md:L129-L133
Useful? React with 👍 / 👎.
The "Reject Raw Merge Conflict Markers" failure on #4409 and #4410 short-circuited `quality-gate` before its Type Check step, so this type debt was never gated. With the markers fixed, mypy reported 138 errors across 42 files -- all 42 of them NEW files added by the consolidated work, none pre-existing on `main`. These are typing fixes only; no runtime behaviour changes. Notably none of them came from the merge: exactly 5 files needed hand resolution (`SPEC.md`, `.secrets.baseline` and three handoff documents) and NOT ONE was Python, so no call site was resolved differently between the two branches. By error code: * `arg-type` (84) -- mostly two patterns. Indexing a 2-D array (`theta[i, 1]`, `y[0]`) yields a scalar at runtime but numpy's stubs type it as `ndarray`, while the callee genuinely needs a scalar because it calls `math.cos`; those sites now hoist `float(...)` locals. The rest are dict literals inferred as `dict[str, object]` and then splatted with `**kwargs`, which defeats keyword type-checking entirely -- fixed with `TypedDict` annotations on the literals (`_StatusCommon`, `_CommonSpecFields`) rather than by loosening any callee signature. Also `tuple(genexpr for i in range(3))` typing as `tuple[float, ...]` instead of `tuple[float, float, float]`, fixed by writing the three elements explicitly. * `no-any-return` (26) -- bound to an annotated local and returned, rather than cast. * `redundant-cast` (18) -- dead `cast(...)` wrappers removed where narrowing already applied, each replaced with an annotated local so the fix also holds where the inner expression degrades to `Any`, and the now-unused `typing.cast` imports pruned so ruff's F401 stays clean. * `var-annotated` (8) -- explicit annotations at the assignment. * `has-type` (1) -- a declaration-only class annotation for a matplotlib base-class attribute that `--follow-imports=skip` cannot see. Verified no class attribute is actually created. * `return-value` (1) -- a documented `cast` where the parameter must stay `object` to remain contravariant with the `CompleteBatchExecution.validator` signature. A cast is a runtime no-op, so the golden-fixture-pinned output bytes are untouched. Two narrowly-scoped `cast`s to `Literal["green", "fairway"]` remain where `TargetDefinition.kind` is declared `str` but `TargetRegion.kind` is a literal union; both carry comments noting `__post_init__` already restricts the domain. Verification, reproducing CI's own file selection (465 files, the changed non-test Python set minus the gate's excluded prefixes) under the pinned `ruff==0.14.10` / `mypy==1.13.0` on Python 3.12 with numpy pinned <2.4 as `requirements.txt` requires: mypy -> Success: no issues found in 465 source files ruff check -> clean (731 gate files) ruff format --check -> clean (731 gate files) `tests/rate_of_closure`: 1167 passed, including the pinned canonical golden-bytes fixture. The two remaining failures are load-induced flakes on this workstation, both passing in isolation -- a loopback-authority readiness timeout that the campaign handoff already documents as happening on a loaded box, and a web-launcher smoke test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`quality-gate`'s "Changed Test Assertion Check" step flagged six `tests/__init__.py` files as changed test files carrying no behavioural assertion. All six are docstring-only package markers for the split swing_sim contract test packages -- `swing_sim/tests` plus the `flight`, `impact`, `putting`, `solver` and `variation` subpackages -- and one is literally empty. They are the same class as `src/shared/python/swing_sim/ground/tests/__init__.py`, which is already allowlisted a few lines above with the same rationale, so this follows the established precedent rather than inventing an exemption. Listed explicitly rather than as a glob, matching how that neighbouring entry is written. Verified: `python scripts/check_test_assertions.py --changed-files <the gate's own 848-file changed list>` now reports "Changed Python test assertion check passed." This step sits after Type Check in the job, so it only became visible once the mypy errors were cleared -- the same masking pattern that hid the type debt behind the raw-conflict-marker failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hitecture test `tests/architecture/test_sidekick_external_imports_3316.py` had been reformatted by the consolidation for purely cosmetic reasons -- the sole change was where `textwrap.dedent(` and its `"""` sit relative to each other. Verified via AST that every string literal is byte-identical between the two forms, so the reformat could not affect behaviour, and verified that BOTH forms pass `ruff format --check` and `ruff check` under the CI-pinned ruff 0.14.10 with the repository's own config. The problem is what that cosmetic edit dragged in. Because CI's `tests` job runs the *changed* test files, reformatting this file enlists an unrelated repo-wide architecture guard into this PR's blocking test set -- and `test_production_code_uses_shared_python_imports` fails on it, reporting duplicate-import violations such as `src/shared/python/sidekick/__main__.py:182: sidekick.launcher_factory`. Those violations are **pre-existing on `main`, not introduced here**: the entire `src/shared/python/sidekick/` tree is byte-identical to `main` on this branch, and the guard's detection is a pure AST walk over `src/` comparing dotted module names, so it is neither path- nor platform-dependent. The test simply never runs on `main` because nothing changes it. Restoring `main`'s exact bytes takes the file back out of the changed set, so this PR stops being gated on unrelated pre-existing debt, with no loss of real work. The sibling guard `test_shared_import_aliases_3316.py` is deliberately left alone -- its diff adds a genuine new `swing_sim` alias-identity test for this work, and it passes (12 passed). Worth a separate issue: that architecture guard is red on `main` and is invisible until someone happens to touch its file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`main` moved to 73fd3d4 when the CI/infra consolidation merged, and `main` is protected with strict required status checks and `allow_update_branch` disabled, so a behind branch cannot land and auto-merge would stall silently rather than report. Merging `main` in explicitly. Two conflicts, both in files #4445 also touched, both resolved as unions: * `SPEC.md` §12 Change Log -- #4445 prepended two dated rows and this branch had prepended one, at the same position. All three are kept, newest first. * `scripts/test_assertion_allowlist.txt` -- #4445 appended the pdf_renamer sub-app conftest entry while this branch appended the six swing_sim package markers. Both blocks retained with their explanatory comments. Worth noting the confluence: #4445 added a `.codex-worktrees/` rule to `.gitignore`, which is exactly the mechanism that stops the six undeclared gitlinks this branch removed from ever landing again. `git diff origin/main -- .gitignore` is zero lines, confirming that rule survived the merge rather than being clobbered. Verified after resolution: no `<<<<<<< HEAD` anywhere outside `.github`, no mode-160000 gitlinks in the tree, and `.gitignore` byte-identical to `main`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Supersedes #4409, #4410
One carrier for both suites. #4409 and #4410 consolidated the same already-closed
#4332-#4402range: they shared ~360 commits, diverged by only 49, and neither was anancestor of the other, so each was re-running the full CI matrix over largely identical
content. Both were red. This branch is
consolidated/rate-of-closure-batchwithconsolidated/ground-study-batchmerged--no-ffon top, with every blocking defect fixed.quality-gatefailed on both PRs at Reject Raw Merge Conflict Markers, despite headcommits on each branch claiming the markers were resolved. That step sits near the top of the
job, so it short-circuited everything after it — which is why fixing it exposed two further
blocking failures in the same content that nobody had ever seen.
1. Raw conflict markers — 5 files
SPEC.mdAGENT_HANDOFF.mddocs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.mdsrc/rate_of_closure/AGENT_HANDOFF.md.secrets.baselineNo source file carried a marker.
Every conflict had one cause: ~25 stacked feature branches each prepended a dated entry
just below a section heading and bumped the same version rows, so every merge collided at
exactly that spot. Because each side is an independent entry rather than a competing edit, the
correct resolution is the union of all sides, with exact-duplicate blocks collapsed.
SPEC.mdneeded a tolerant parser: prior "resolutions" had deleted markers arbitrarily,leaving 14 orphaned
=======and 31 orphaned>>>>>>>lines with no matching<<<<<<<.It was verified line-for-line afterwards that no original content was dropped from any of
the four documents. The single-valued Identity rows collapse from 33 / 33 / 6 duplicates to
one each, at the highest observed version (
1.14.94) plus a bump to1.14.95.2.
.secrets.baselinehad Windows path keysThe baseline inherited from #4410 was regenerated on Windows and committed with backslash
path keys (30 of 33), replacing the POSIX keys on
mainand #4409. No Linux CI scan can matcha
docs\SECRETS_MANAGEMENT.mdkey, so thedetect-secretsjob would have reported every realaudited entry as a newly added secret. Regenerated with CI's exact invocation and POSIX keys;
the result is a superset of both
main's and #4409's entries and is idempotent under rescan.3. ruff-format version drift — 100 files
The branches were formatted with a ruff other than the pinned
ruff==0.14.10, which reformatted100 changed Python files into output the pinned formatter rejects — including files with no
connection to this work (
launch.py,scripts/bump_vendor_pin.py,src/shared/**). This wasnot pre-existing debt:
main's versions of all 91 of those files that exist there passruff format --checkunder 0.14.10 cleanly. Re-running the pinned formatter restores 85 ofthose 91 to byte-identical-to-
maincontent; the other 6 keep genuine changes.4. A rewrite that contradicted its own suppression
tests/unit/sidekick/agent/test_action_audit.pyhad an automated UP017 fix rewritetzinfo=timezone.utctotzinfo=UTCand importUTCfromdatetime, while leaving in placethe very suppression forbidding it:
# noqa: UP017 - Python 3.10 CI lacks datetime.UTC.Thetestsmatrix still includes Python 3.10, wheredatetime.UTCdoes not exist. Reverted. Therest of the branch was swept: every other
from datetime import UTCsits inside a guardedsys.version_infocompatibility shim.5. Accidental tree entries, absent from
main.codex-worktrees/*— six mode-160000gitlinks pointing at stale local Codex worktreesfrom June (PRs Fix Swingset legend plot clearance #3602-[codex] Seed transfer entropy permutation tests #3784, long since merged). There is no
.gitmodules, so they areundeclared, and the commits they reference exist only in one local clone and are on no
remote branch — a fresh
git clonewould get six gitlinks to objects the server does nothave. This is the "unregistered historical gitlink" that the campaign handoff records as
having broken
actions/checkout's cleanup and forced a credential-free manual fetchworkaround.
batch_plan.txt,non_draft_prs.json,pr_worker_final_results.json,specified_summary.txt— rawghAPI dumps and PR-batching notes written into therepository root by the prior consolidation agent. CRLF-encoded, and they trip
git diff --check.6. 138 mypy errors in 42 files, previously masked
With the markers fixed,
quality-gatereached its Type Check step for the first time andreported 138 errors across 42 files — all 42 of them new files added by this work, none
pre-existing on
main. None came from the merge: exactly 5 files needed hand resolution(§1 above) and not one was Python, so no call site was resolved differently between the two
branches.
arg-type(84) — two dominant patterns. Indexing a 2-D array (theta[i, 1],y[0])yields a scalar at runtime but numpy's stubs type it as
ndarray, while the callee genuinelyneeds a scalar because it calls
math.cos; those sites now hoistfloat(...)locals. The restare dict literals inferred as
dict[str, object]and then splatted with**kwargs, whichdefeats keyword type-checking entirely — fixed with
TypedDictannotations on the literalsrather than by loosening any callee signature.
no-any-return(26) — bound to an annotated local and returned, rather than cast.redundant-cast(18) — deadcast(...)wrappers removed where narrowing already applied,each replaced with an annotated local so the fix also holds where the inner expression
degrades to
Any, with unusedtyping.castimports pruned soF401stays clean.var-annotated(8),has-type(1),return-value(1) — explicit annotations, onedeclaration-only class annotation, and one documented
castwhere the parameter must stayobjectto remain contravariant with theCompleteBatchExecution.validatorsignature. A castis a runtime no-op, so the golden-fixture-pinned output bytes are untouched.
7. Two more masked steps
tests/__init__.pypackage markers undersrc/shared/python/swing_sim/**failedChanged Test Assertion Check; allowlisted alongside the equivalent
ground/testsmarkeralready listed there.
tests/architecture/test_sidekick_external_imports_3316.pyhadenlisted an unrelated repo-wide import-boundary guard into this PR's blocking test set, and
that guard fails against
main's own tree — thesrc/shared/python/sidekick/tree here isbyte-identical to
main, and the guard is a pure AST walk oversrc/comparing dotted modulenames, so it is neither path- nor platform-dependent.
main's exact bytes are restored so thefile leaves the changed set. The guard's own failure is tracked as separate follow-up work.
Content integrity
src/rate_of_closure, 111 undertests/rate_of_closure.files are 5
__init__.pypackage markers.main, and both are thedocumented module-budget extraction rather than deletion — the
src/rotation_converter/ui/pyqt6/main_window.pyhelpers now live inplot_helpers.py, andmotion_tabs.pyhelpers inmotion_helpers.py. Both new modules define and export the movednames.
Gate evidence, pinned toolchain
Reproduced against CI's own selections —
ruff==0.14.10,mypy==1.13.0, Python 3.12, numpypinned
<2.4asrequirements.txtrequires:ruff check— clean (731 gate files)ruff format --check— clean (731 gate files)mypy— Success: no issues found in 465 source filesthis branch:
coverage_gate_requiredonly turns true when atracked_packagespath changes,and this branch touches none.
Known non-blocking failure
file-size-budgetfails: three new GUI modules exceed the 500-LOC budget and are not inscripts/monolith_baseline.txt—torque_profile_panel.py(612),regional_ground_execution_workspace.py(572),rate_of_closure/ui/pyqt6/main_window.py(541).It is not a required check, and the campaign handoffs already record the 612-line file as known
debt, so it is reported rather than refactored inside a consolidation.
Already-closed PRs this carries
All 32 are closed unmerged, so their work exists only on these consolidation branches.
From #4409 — Ground Study (21): #4332, #4335, #4350, #4351, #4352, #4353, #4354, #4355,
#4356, #4357, #4359, #4360, #4361, #4365, #4366, #4367, #4370, #4372, #4373, #4374, #4375
From #4410 — Rate of Closure (11): #4339, #4342, #4358, #4362, #4363, #4364, #4368, #4371,
#4376, #4388, #4402
Issues referenced
All open, and each is annotated with this PR. None are closed by this merge — the campaign
handoffs list remaining work under every one of them, and this PR deliberately carries no
closing keywords.