Skip to content

Variation / Morris Sensitivity Suite: consolidate 34 draft PRs (#4142, #4433, #4120, #4206) - #4447

Open
dieterolson wants to merge 337 commits into
mainfrom
consolidated/variation-morris-2026-08-13
Open

Variation / Morris Sensitivity Suite: consolidate 34 draft PRs (#4142, #4433, #4120, #4206)#4447
dieterolson wants to merge 337 commits into
mainfrom
consolidated/variation-morris-2026-08-13

Conversation

@dieterolson

@dieterolson dieterolson commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Consolidates the variation / Morris-sensitivity draft family (34 open draft PRs) into one branch against main.

Known non-required-lane state, disclosed rather than left to be discovered: tests (3.12) reports 6 failed / 4893 passed. Three are main's own pre-existing failures, fixed in #4480 (main fails them today at edfeae684; see #4481). The other three are this family's, fail only on 3.12, and pass on 3.11 — filed as #4482 (an over-tight cross-client parity fixture, the Morris authority child never reporting readiness, and a JSON error taxonomy that shifts with the interpreter). tests (3.12) is not a required check; none of the six is fixed here, per the rule that a consolidation is the wrong vehicle for pre-existing defects.

Correctness findings are now filed as issues — they outlive this consolidation and are not fixed here, because a consolidation is the wrong vehicle for pre-existing defects:
#4455 (Morris validation is scale-blind — highest value) · #4456 (variation_parity.json vacuous) · #4457 (three flight-model policies) · #4458 (parity-only tests) · #4459 (tautological router verification) · #4460 (tests (3.11) never created on any of the 34 drafts) · #4461 (unpinned Python/TS duplicates) · #4462 (suite cannot detect a planted-design substitution)

Supersedes

Every PR below has been verified by ancestry check to be a true ancestor of this branch, so closing it drops nothing. Verified with verify_coverage.sh: covered=34 NOT-covered=0 SAFE, and independently against each PR's current headRefOid from the API (not from a cached branch table).

Supersedes #4393, Supersedes #4394, Supersedes #4395, Supersedes #4396, Supersedes #4397, Supersedes #4398, Supersedes #4399, Supersedes #4400, Supersedes #4401, Supersedes #4403, Supersedes #4404, Supersedes #4405, Supersedes #4407, Supersedes #4412, Supersedes #4413, Supersedes #4414, Supersedes #4415, Supersedes #4416, Supersedes #4417, Supersedes #4422, Supersedes #4423, Supersedes #4424, Supersedes #4425, Supersedes #4426, Supersedes #4427, Supersedes #4428, Supersedes #4431, Supersedes #4432, Supersedes #4435, Supersedes #4440, Supersedes #4441, Supersedes #4442, Supersedes #4443, Supersedes #4444

Epics

This family closes no leaf issues. It advances epics only:

Part of #4142
Part of #4433
Part of #4120
Part of #4206

(#4206 arrives via #4417, which had absorbed the already-merged #4436.)

Why merging the 7 chain tips was not sufficient

The brief assumed a chain tip contains everything below it. That is false here. #4417's branch head advanced after its six children branched off it: it now carries 89563be74 + 87719a266 (Sasho rotational AoA) via a merge of #4436, which is itself already MERGED into that feature branch and exists on no other branch and not on main. Those two commits are absent from all seven tips. They are merged here explicitly, so #4417 can be closed without stranding #4436's work.

#4424 and #4423 reported Already up to date — they were genuinely contained by the other tips. Confirmed by ancestry, not by assumption.

Blocking defects fixed

  1. Unreachable gitlinks (the repo-wide blocker). Removed the six mode-160000 entries under .codex-worktrees/. They are not declared submodules (no .gitmodules anywhere) and the commits they point at exist only in one local clone, on no remote. main has zero of them. Verified: git ls-tree -r HEAD --name-only | grep '^\.codex-worktrees' is empty, and no 160000 mode remains anywhere in the tree. The .gitignore line that prevents recurrence is owned by the CI/infra consolidation, not this branch.

  2. quality-gate Format Check would have failed on 91 files. Slices in this stack reformatted files with an older ruff, reverting them to the pre-0.9 wrapping style (assert hasattr(\n a, b\n), "msg" instead of assert hasattr(a, b), (\n "msg"\n)). I confirmed origin/main is format-clean under the CI-pinned ruff 0.14.10 while this branch was not, so the regression is the family's. Only 1 of the 91 files is rate_of_closure/variation code — the other 90 are collateral churn in unrelated trees. Reformatted with 0.14.10. The CI-excluded prefixes were deliberately left untouched.

  3. ruff check would have failed with 9 × F841. A slice wrapped nine app = QtWidgets.QApplication… statements across three lines in tests/unit/sidekick/test_sidekick_ux_hardening.py, moving # noqa: F841 off the line ruff reports the diagnostic on. Restored main's single-line form.

  4. tests (3.10) would have failed at collection. tests/unit/sidekick/agent/test_action_audit.py had been rewritten to from datetime import UTC, which does not exist on Python 3.10 — a regression of the fix SPEC 1.1.352 recorded for this exact file. Restored main's timezone.utc form. Root cause worth a follow-up: the repo's AST guard tests/rate_of_closure/test_python_310_datetime_boundaries.py only walks src/rate_of_closure, so it structurally cannot catch this class of regression in tests/ or in any other src tree.

  5. No raw conflict markers: git grep "<<<<<<< HEAD" is empty.

  6. quality-gate Type Check (Mypy) failed with 70 errors in 27 files — and every one of those 27 files is a file that does not exist on main at all. 428 of the 537 changed source files here are new. Because each source PR was based on its parent's branch, its changed-file delta only ever contained its own slice; once a file was introduced in slice N, no later slice re-checked it, and no PR in the family ever type-checked it against main. That is why all 34 sources show a green Mypy step while the consolidation did not. Fixed all 70 — 16 numpy no-any-return/var-annotated (annotated locals), 16 arg-type in _kinetics_dynamics.py (explicit float() at scalar-indexing boundaries), 16 arg-type from dict[str, object] inference being **-unpacked into typed constructors, 5 attr-defined/index in launcher.py (root cause: the unannotated GUI_INFO literal), 4 redundant-cast, plus one each of has-type, Literal narrowing, FrozenJsonValue, and an untyped Qt currentData(). All behaviour-preserving; the one public signature I briefly tightened was reverted in favour of a call-site fix.

  7. quality-gate Changed Test Assertion Check failed on six src/shared/python/swing_sim/**/tests/__init__.py package markers (five docstring-only, one empty). Added them to scripts/test_assertion_allowlist.txt, following the check's own guidance and the existing src/movement_optimizer/tests/** precedent.

  8. pyproject.toml and tool_manifest.yaml had been converted from LF to CRLF by this family, so both rendered as near-total rewrites. Restored LF: their real change is 35 added lines, not 802 changed ones. These were the only two files in the 1249-file diff with a newly introduced CRLF conversion.

  9. The web build was broken, and a real behavioural regression was hiding behind it. npm run build (i.e. tsc) failed, which is why "Rate web production Worker E2E" could not even start its preview server — that check passes on all four source PRs that carry it, so the breakage is the integration, not any one slice. Two distinct cross-slice defects:

    (a) Type error. feat(variation): bind runtime replay identity #4431 made executionMetadata required on VariationExecutionRequest/VariationExecutionResult, but four test fixtures introduced by the feat(rate): add bounded club explorer camera #4444 chain construct those objects without it. Supplied it the way the already-updated fixtures in the same file do — from the request under test, which also satisfies validateResult's request/result metadata equality check.

    (b) A user-visible regression, and the most consequential thing I found. importPlan reports provenance after loading a plan — either "resolves against the current variable registry … not evidence of historical reproducibility" for a raw plan, or "Execution sidecar verified against the current registry" for a signed document. The feat(rate): add bounded club explorer camera #4444 chain added an effect that calls invalidateResults on every configuration-identity change; loading a plan changes that identity, so the effect ran after commit and overwrote the provenance message with the generic "Ready: configuration changed; run again." The user was silently denied the one message that tells them a loaded plan was re-resolved against today's registry rather than its recorded one — an honesty regression in a suite whose whole theme is honest provenance. invalidateResults now announces only a discard that actually happened, decided from refs so the callback identity stays stable and the effect cannot re-trigger itself. Nothing else references that string, and the invalidation tests assert visual state and heading removal rather than status text.

    Two React tests (from feat(variation): bind runtime replay identity #4431's side) correctly detected (b) and were failing; they now pass. Web workspace: tsc clean, eslint --max-warnings 0 clean, vite build succeeds, vitest 1084/1084 across 126 files (was 1082/1084).

  10. The family's Python test suite had never run in CI at all, and once run it revealed five more cross-slice contract contradictions. pick-runner never granted any of the 34 drafts a fleet slot, so tests (3.11) was never even created on a single one of them — every "green" source PR was green on quality-gate alone. I built a Python 3.11 environment with CI's pinned numpy/scipy/pandas/PyQt6 and ran the suite. Five real failures, none caused by the merge; each is one slice's contract rejecting another slice's code, where no branch contained both halves:

    (a) The RNG-replay contract vs the explicit-design seam — the most serious. feat(rate-of-closure): produce paired localized attribution #4426 added build_simulation_ensemble_request_from_samples for "deterministic experimental designs whose rows are the scientific authority … rather than pseudorandom Monte Carlo draws". feat(variation): bind resolved execution metadata #4428/feat(variation): bind runtime replay identity #4431 then made SimulationEnsembleRequest require unconditionally that sampled_inputs equal sample_inputs(plan). Together the request contract rejects exactly the designs the seam exists to carry: 7 tests failed and the entire paired-attribution producer was non-functional. Reconciled by making the provenance explicit — sample_provenance defaults to plan_rng so every existing caller keeps the reproducibility guarantee bit-for-bit, and the explicit seam declares explicit_design, which skips only the RNG-equality check. Shape, finiteness, and _require_config_sample_binding — the check that actually proves each row was executed as written — all still apply. The same contradiction also broke a benchmark script and two archive tests.

    (b) A run may no longer be attributed to a config it did not come from. capture_simulation gained a config-identity guard; a Morris test injected pre-computed runs built from other configs. The guard is correct, so the test changed: the hit-availability case now asserts against project_simulation_outcome, which owns that rule, and the miss goes through the real evaluator end to end.

    (c) NaN where a null belongs. launch_monitor_import returned NaN for an empty CSV cell, because from pandas 3 a bare DataFrame construction coerces None in an object column. The test's own name says this import mirrors browser policy, which yields null. Built with dtype=object so an absent value stays absent instead of silently becoming a floating-point quantity.

    (d) Eight paired-study controls had no tooltips, failing the repo-wide tooltip-completeness contract that a different slice owns.

    (e) A request-identity test asserted identity by mutating sampled inputs in place, which is now unconstructable twice over. The sample-bytes case rebinds each config to its row; the config-order case now asserts the stronger property the binding provides — a misordered request is rejected outright rather than merely hashing differently.

    Result: tests/rate_of_closure/ is 1451 passed, 0 failed on Python 3.11. One further failure reproduced only on 3.13 (a JSON nesting-depth error-taxonomy difference) and passes on 3.11, so it is untouched.

Local reproduction note

Reproducing quality-gate on Windows requires MYPYPATH=src;src/python/src with a semicolon. CI is Linux and uses :; on Windows that colon string parses as one nonexistent directory, every first-party import silently degrades to Any under --ignore-missing-imports, and mypy reports a fabricated result. I measured both on this branch's exact 418-file list: semicolon gives Success, colon gives 109 errors in 13 files — a different, bogus set. Cross-module resolution also requires passing all 418 files to a single invocation via mypy's @listfile form (not xargs, which splits on Windows), numpy <2.4, and mypy pinned to 1.13.0. My initial semicolon run reproduced CI exactly: the same 27 files and 70 errors.

Substantive conflict resolutions

Two conflicts were more than textual, and both were resolved against the incoming side.

request_builder.py — a mis-merge that would have silently destroyed the paired-attribution design. Git aligned #4431's Monte Carlo function body against the #4444 chain's new explicit-sample seam, so the incoming side inserted

samples = sample_inputs(plan)

inside build_simulation_ensemble_request_from_samples — the function whose whole contract is that its caller-supplied rows are the scientific authority (planted baseline/perturbed pairs). Taking that side would have replaced an explicit design matrix with pseudorandom draws while every test still passed. Rejected. Execution metadata is now supplied by the sampling caller and stays None on the explicit seam: the metadata carries an RNG replay identity, and rows never drawn from that stream cannot be replayed from it, so claiming it would be a false provenance assertion.

simulation_adapter.pyassert execution_metadata is not None became a None-tolerant identity override for the same reason. An injected executor still can never be reported as the pinned production implementation.

Remaining resolutions, all union-of-both-sides: attribution wiring re-homed from the eager _on_ensemble_succeeded into _apply_prepared_result so it publishes only for an accepted result (guarded by authority() is None per #4427, and not cleared by _clear_result_widgets, so explicit paired authority keeps its own lifecycle); EnsembleStreamHeader fields and validations; the TS execution-metadata contract (the prepared request now feeds both validateExecutionRequest and validateResult); #4425's flight-model check kept alongside the stricter isSafeInteger nRuns check; #4427's value-based scenario comparison (strictly narrower than the identity check it replaced); and the dated sections in SPEC/handoff docs.

Correctness findings — read before merging

These are defects in the consolidated work, not in the consolidation. None is a merge error. I am reporting them rather than fixing them because each is a design decision for the epic owner.

1. There is no TypeScript Morris implementation, despite a branch named codex/4142-morris-typescript-parity. The Morris numerics exist in exactly one place: src/shared/python/swing_sim/variation/global_sensitivity.py. The TypeScript side is a wire parser plus an algebraic invariant checker. src/rate_of_closure/variation/morris_rate_adapter.py and morris_execution.py contain zero effect arithmetic. So the recurring Python/TS algorithm divergence cannot occur here — but the "parity" framing overstates what exists, and there is no second implementation to cross-check against.

The TS check that does exist is genuine, not decorative: morrisMetricValidation.ts:39-63 enforces sigma² − n·SE² − c·muStar² + c·mu² ≈ 0 with c = n/(n−1), which is an exact identity given Python's ddof=1 estimators, and would fire if Python switched to ddof=0. Zero-clamp thresholds match exactly (64 * eps * max(1, muStar) on both sides). Its limitation is structural: it validates a relation among metrics, so a uniformly wrong scale factor passes.

2. The Morris estimator has exactly one genuine numeric validation, and it is scale-blind. test_global_sensitivity.py:89-114 uses response 2·x₀ + 3·x₁ over bounds [0,1], so closed-form elementary effects are exactly 2 and 3. That is real ground truth. But every numeric mu_star assertion in the repo runs on factors with lower=0.0, upper=1.0, where normalized and physical coordinates coincide. Elementary effects are computed per normalized factor range (global_sensitivity.py:173 divides by signed_steps, which are in [0,1] units), and no test can currently distinguish "per normalized range" from "per physical unit." test_morris_rate_adapter.py does use non-unit bounds but contains zero mu_star assertions. This is the highest-value gap I found.

3. Parity-only tests (agreement asserted, correctness not). morrisUiParity.test.ts (both tests), test_morris_ui_contract.py::test_python_generated_ui_fixture_is_exact_and_shared, test_web_parity_fixture.py (both tests), variation.test.ts::"matches the fixture dispersion within the loose band", morrisGlobalSensitivityContract.test.ts::"parses the Python golden fixture…", morrisPresentation.test.ts::"ranks finite effects…" (its muStar 3 and 2 are read from the fixture), and the morris_workspace_v1.json consumers. One test is mislabeled as parity and checks nothing cross-runtime: morrisAuthorityRequest.test.ts::"matches Python R13.6 base-centered suggestions…" reads no Python artifact — its ten expected bound pairs are hardcoded TS literals, so Python drift cannot fail it.

To be fair to the fixtures: morris_ui_parity_v1.json and morris_global_sensitivity_golden_v1.json are genuinely two-sided (each is pinned by a Python test and a TS test, so a one-sided change fails exactly one), and no regeneration script exists for any fixture, so none is auto-derived from one side. The golden fixture's mu_star values coincide with the analytically-asserted 2.0/3.0, making it the one fixture transitively anchored to closed-form truth.

4. variation_parity.json is effectively vacuous as a correctness gate. The TS side accepts mean_abs_tolerance of 8.0 m on a 216.73 m mean (3.7%) and std_rel_tolerance of 0.4 (40%). A browser engine with a 39% dispersion error passes. The Python test's own docstring concedes exact RNG parity is not attempted.

5. #4425's flight-model pin contradicts the Morris slices in this same stack. Three policies now coexist:

Surface Allowed flight models
Python VariationPlan (spec.py:283) all 7 FlightModelType members
variationSchema.ts validatePlan 1 (waterloo_penner)
morrisAuthorityRequest.ts AUTHORITY_FLIGHT_MODELS all 7

A plan with flight_model in {macdonald_hanzely, nathan, ballantyne, jcole, rospie_dl, charry_l3} is valid in Python, round-trips through to_json_dict, and is rejected by the browser — and accepted by the Morris path in that same browser. The reverse cannot happen. #4425's error message ("unsupported for browser variation") makes this honest rather than silent, and #4425's Python-side change is sound (it requires plan and config to agree, without restricting the set). But the browser is now internally inconsistent, and SUPPORTED_VARIATION_FLIGHT_MODEL has no test asserting the rejection or documenting the asymmetry.

6. normalized_step can be reported inaccurately and neither runtime can detect it. _validate_design_paths never requires |signed_steps| == delta, nor that points lie on the k/(levels−1) lattice. TS parseDesign re-derives levels/(2·(levels−1)) from levels — validating the field against itself, not against the divisor actually used. MorrisResults.tsx then displays that number to the user.

7. Smaller divergences, all currently latent but not guarded: the Morris workspace CSV export has 39 Python columns vs 35 TS, with different order and different names for the same data (source_time_window_start_s vs source_time_start_s, design_levels vs levels, …) and four Python-only columns — each side asserts only its own header, and nothing compares the two. OUTPUT_LABELS is duplicated (23 Python entries vs 17 TS) and already out of sync. variationRegistry.ts is a hand-mirror of registry.py, including the typicalScale values that drive every Morris suggested bound, with no test pinning the two together (I compared the 10 Morris entries by hand — they agree today; nothing keeps them agreeing). Presentation tie-breaks differ (Python codepoint spec_id vs TS localeCompare), as do fallback labels (str.title() vs first-char capitalization: "carry_2m""Carry 2M" vs "Carry 2m"). Finally, application/morris/router.py:238 "verifies" a result by calling analyze_morris on the same inputs and comparing — tautological; it can catch transport corruption but never an algorithm error.

What I could not verify

  • I ran the JavaScript suite but not the Python suite. The web workspace is fully exercised locally (vitest 1084/1084, tsc, eslint, vite build).
  • The Python suite now runs locally on 3.11 and is green (1451 passed), including the PyQt paths I re-homed. PyQt6 6.11 does import on this machine, so those tests executed rather than being reasoned about. What I did not run locally is the rest of the repo's suite outside tests/rate_of_closure/, the Rust wheel build, or the Playwright PyQt-DPI lane.
  • My decision to leave execution_metadata=None on the explicit-sample seam is a judgement call, not something a test forced. It is the honest reading (no RNG was used, so no RNG replay identity should be claimed) and it is consistent with the stack's own "model identity honesty" theme, but if the epic owner intends explicit designs to carry metadata, it needs a non-RNG provenance variant rather than reusing make_execution_metadata.
  • Four files under src/data_processing/ are changed by this family and are not ruff-format-clean under 0.14.10. They sit under a CI-excluded prefix, so the gate does not see them, and I left them untouched per the exclusion rule. Flagging so it is not mistaken for clean.
  • I reviewed the Morris and attribution numerics and the flight-model policy. I did not audit the club-explorer camera, Putting sample inspector, linked-scatter, or visualization-manifest slices from the Epic: Visual-first tab visibility and visualization-led React/PyQt UX #4433 series beyond conflict resolution.

Gates run locally

Reproduced with CI's own pinned tools, commands, and file lists:

  • ruff check (pinned 0.14.10), 596 in-scope changed files and full-repo: clean.
  • ruff format --check (pinned 0.14.10): clean.
  • mypy (pinned 1.13.0, CI's exact 418-file list and flags): Success, no issues.
  • scripts/check_test_assertions.py --changed-files: passed.
  • scripts/check_minimum_test_contract.py: passed.
  • scripts/check_module_size_budget.py --max-lines 1200 --include src: passed.
  • bandit -ll -ii over the 372 changed src files: 0 medium, 0 high.
  • git grep "<<<<<<< HEAD": empty. git ls-tree -r HEAD | grep '^160000': empty.
  • verify_coverage.sh: safe=34, NOT-covered=0, SAFE.

requirements.txt is untouched, so pip-audit is unchanged from main. file-size-budget is not a required check here, and I have not refactored any module to satisfy it.

Merge note

main requires linear history, so this must land via squash, not a merge commit. Squashing collapses 34 PRs' worth of slice history, so the provenance above is the only surviving record — please keep it in the squash message.

🤖 Generated with Claude Code

dieterolson and others added 30 commits August 6, 2026 02:58
# Conflicts:
#	SPEC.md
#	src/rate_of_closure/ui/pyqt6/simulation_view.py
…r-analytics

feat(rate-of-closure): add launch monitor analytics tabs
…y' into feat/4193-launch-direction-registry-integration
…ion-registry-integration

feat(rate-of-closure): add launch direction conventions
* feat(swing-sim): add spatial target contract (#4192)

* feat(flight): add canonical result metric catalog

* feat(ball-flight): add reproducible wind comparison

* feat(flight): add desired-flight inverse solver

* fix(ci): satisfy Python 3.12 delta typing

* feat(ball-flight): analyze wind estimate uncertainty

* feat(ball-flight): add interactive 3d playback

* fix(ball-flight): distinguish apex and landing events

* feat(flight): add impact solution families

* feat(flight): add capability-aware shot optimizer

* feat(flight): add variability and downside objectives

* fix(ball-flight): preserve responsive plot aspect ratio

* fix(ball-flight): preserve playback canvas aspect ratio

* style(rate-of-closure): format analytics modules

* refactor(rate-of-closure): split flight explorer controls

* fix(rate-of-closure): preserve typed explorer values

* docs(ball-flight): reconcile integration specification

* docs(rate-of-closure): add campaign integration handoff

* fix(ball-flight): harden wind uncertainty risk metrics

* docs(rate-of-closure): record wind strategy v2 handoff

* docs(rate-of-closure): record draft integration PR

* docs(rate-of-closure): verify wedge AoA worked example

* style(rate-of-closure): satisfy PR Ruff format gate

* docs: checkpoint variation and wedge campaign handoff

* fix(rate-of-closure): support pinned CI mypy

* docs: record pinned CI mypy recovery

* refactor(rate-of-closure): split simulation tab controller

* docs(rate-of-closure): record base sync recovery

* docs(rate-of-closure): record rendered design audit

* feat(rate-of-closure): complete spatial target workflows and responsive layout

* docs(rate-of-closure): refresh campaign handoff evidence

* fix(rate-of-closure): satisfy pinned CI type contracts

* docs(rate-of-closure): record pinned CI compatibility fix

* fix(rate-of-closure): narrow target frame for pinned mypy

* docs(rate-of-closure): record final pinned CI correction

* fix(rate-of-closure): reserve themed wrapped form height

* docs(rate-of-closure): record themed layout verification

* fix(variation): preserve paired landing rows

* docs(wedge): separate physical and synthetic examples

* fix(ci): type NumPy variation intermediates

* feat(rate-web): add frame-explicit manual delivery controls

* feat(rate-of-closure): declare manual delivery geometry

* docs(rate-web): declare manual impact model boundary

* test(rate-of-closure): pin wedge manual delivery parity

* fix(rate-web): validate current run and delivered loft

* fix(rate-of-closure): preserve manual delivery precision

* fix(rate-of-closure): gate source-specific pose controls

* fix(rate-web): enforce active model contracts

* fix(rate-web): fail closed on incomplete v5 settings

* fix(rate-of-closure): reject truncated v5 run settings

* fix(rate-web): require canonical v5 ball setup

* fix(rate-of-closure): align visible simulation inputs

* fix(rate-of-closure): reject unemitted schema versions

* docs(swing-sim): clarify automatic backend fallback

* fix(rate-of-closure): keep compact controls readable

* fix(rate-of-closure): prevent shaft datum truncation

* docs(rate-of-closure): record validated campaign handoff
codex-scheduled added 2 commits August 14, 2026 12:42
…mplete

`tests (3.11)` failed at COLLECTION, which aborts the whole run before any test
executes -- hence all three matrix lanes failing identically:

    ERROR collecting tests/rate_of_closure/test_morris_authority_child.py
    ModuleNotFoundError: No module named 'uvicorn'

Declared the dependency rather than restructuring the test or adding a skip
guard. A `pytest.importorskip` here would make the Morris authority tests
silently vanish in CI, which is the same "check that reports success while
checking nothing" pattern filed as #4460; fixing a red gate must not create
another one. It is also not an option here: the test monkeypatches
`child.uvicorn` directly, so the module attribute has to exist.

`uvicorn` is already declared in the `rate-morris-authority` and `all` extras in
pyproject.toml, but neither job installs those extras -- both install
`requirements-ci.txt` plus an explicit `fastapi python-multipart` line, which is
where `fastapi` is effectively declared for CI. Added `uvicorn` there, in both
the quality-gate and tests jobs, since both build the same environment.

Auditing the same class across the branch found one more, which would have
failed as soon as collection got past the first: `psutil` is imported at module
scope by `scripts/benchmark_rate_ensemble_archive.py`, which
`test_variation_ensemble_archive_memory.py` runs as a subprocess with the same
interpreter. It is declared in NO requirements file, pyproject extra, or
workflow anywhere in the repo, and the test has no guard, so a missing psutil
fails it rather than skipping. Declared alongside uvicorn.

Everything else the audit flagged is safe: `build123d` is guarded by
`pytest.importorskip` in both wedge tests, `swing_core` by try/except in
`_rust_facade.py`, `starlette` ships with fastapi, `mpl_toolkits` with
matplotlib, `jax`/`pendulum_core` live in the pendulum lane CI excludes from
changed-tests, and the rest are first-party packages.

Chose the workflow install line over `requirements.txt` deliberately: it matches
how fastapi, pymodbus, sqlmodel and httpx are already handled, and avoids making
`requirements-lock.txt` stale and widening pip-audit's scope.
Two conflicts, one of them a genuine divergent refactor of the same file.

**src/movement_optimizer/gui/motion_tabs.py** — both branches split this module
to satisfy the module-size budget, but differently. This branch extracted
`motion_helpers.py` (`build_motion_colors`, `chain_path_length`); #4438 extracted
`motion_view.py` (`MotionViewMixin`) and kept a local `_build_motion_colors`.
Took main's version wholesale and deleted the now-orphaned `motion_helpers.py`,
which nothing else imported. That reproduces main's exact, reviewed, green state
for a vendored sub-app that CI excludes from every ruff, mypy and changed-test
lane -- i.e. a place where a mis-resolution would not have been caught.

Worth recording: git presented the third hunk as main *deleting* the "Playback
speed" control, and it does not. Main keeps it at line 682 as a single-line
`_add_control` call while this branch had it line-wrapped. Both branches'
`test_motion_tabs.py` are byte-identical and both reference
`chain._controls["speed"]`, so dropping it would have broken the sub-app
silently. Verified before resolving rather than trusting the conflict markers.

**torque_utils.py** — rerere replayed this branch's resolution, which routes
polynomial evaluation through the shared `TorquePolynomial` instead of
`np.polyval` on reversed coefficients. Since the two use opposite coefficient
orders, I checked equivalence numerically rather than by reading: ascending
`TorquePolynomial(coeffs).evaluate(t)` matches `np.polyval(coeffs[::-1], t)` to
1e-12 across t in {0, 0.13, 0.5, 1.0, 2.5}.

Re-verified on the new base: mypy clean over CI's 418 files, ruff check and
format clean over 596, module size budget, changed-test assertion and minimum
test contract gates all pass, no conflict markers, no gitlinks, and CI's exact
210 changed-test files now collect cleanly -- 2572 tests, exit 0.
dieterolson pushed a commit that referenced this pull request Aug 14, 2026
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.
codex-scheduled added 2 commits August 14, 2026 14:51
…4475)

`.github/workflows/ci-standard.yml` auto-merged cleanly -- #4475 changes the
`pick-runner` job's placement while this branch's change is in the dependency
install lines, so the two do not overlap. Verified both survived: the hosted
dispatcher routing is at line 83 and `uvicorn psutil` at lines 246 and 607.

This is a welcome merge rather than just a required one: #4475 moves
`pick-runner` off `d-sorg-fleet` onto a hosted runner for public repos, so the
dispatcher no longer burns a fleet slot purely to decide placement while every
job with `needs: pick-runner` queues behind it. That is precisely what has been
holding this PR's `tests (3.11)`.

One conflict, and it was not a content conflict.
`.github/workflows/cross-repo-python-integration.yml` came through as a
whole-file conflict because **main's copy is CRLF (256 lines) while this
branch's -- and the repo convention, `core.eol=lf` -- is LF (252 lines)**. Git
could not align a single line, so it presented both copies in full. Normalizing
for comparison shows the real change is 11 insertions and 7 deletions: #4475's
`CI_RUNNER_MODE`/`REPOSITORY_PRIVATE` plumbing, the public-repo hosted fallback,
and the removal of `vendor/ud-tools` from the sparse checkout.

Took main's version verbatim, CRLF included. Normalizing it to LF here would be
the correct convention but would add a 252-line line-ending diff to a
consolidation that does not otherwise touch this file, and the regression is
main's rather than this family's. Flagged for a separate one-line fix instead --
left as is, every other in-flight consolidation branch will hit this same
unalignable whole-file conflict.

Re-verified on the new base: mypy clean over 374 files, ruff check and format
clean over 584, assertion and module-size gates pass, both workflows parse as
valid YAML, no conflict markers, no gitlinks.
…step

I resolved the previous merge wrong. Taking main's
`cross-repo-python-integration.yml` wholesale to avoid a line-ending diff also
discarded two of this branch's own changes, because the file's content had
genuinely diverged and not only in #4475's hunks:

- `vendor/ud-tools` in UpstreamDrift's sparse-checkout scope
- the whole `Initialize pinned Tools submodule` step

This branch's `tests/ops/test_cross_repo_python_integration.py` asserts both
(it adds 27 lines main does not have, including a dedicated test that the
submodule step runs before dependency install), so I kept the tests while
dropping the thing they test. `tests (3.12)` caught it.

Rebuilt as the actual union: started from this branch's version and applied
#4475's two hunks (`CI_RUNNER_MODE || LOCAL_RUNNER_MODE` plus
`REPOSITORY_PRIVATE`, and the public-repo hosted-runner fallback). Written with
CRLF to match main so the diff against main is the 8 lines of real content
rather than a whole-file line-ending rewrite. Verified all three are present and
the submodule test passes again.

Two failures remain in this file's tests, and they are NOT from this branch:
`origin/main` fails the identical two. Proven by running them in a detached
worktree at edfeae6 -- `2 failed, 3 passed`. Main's own
REQUIRED_SPARSE_PATHS expects `shared`, `sidekick`, `python/src/utils`,
`contracts.py` while main's workflow declares the `src/`-prefixed forms. Main is
green only because this test file is not in main's changed-file set, so its
delta-CI never runs it -- the same blindness filed as #4460. This branch merely
touches the file and so inherits the failure.

Not fixing main's two here, per the standing rule that a consolidation is the
wrong vehicle for pre-existing defects. Reported for routing instead.
…nst recurrence

`tests (3.10)` aborted at COLLECTION with 18 errors, so the whole lane never ran
a single test. Two 3.11+ stdlib features were imported at module scope:

- `enum.StrEnum` in `club_camera.py` and `variation_visual_state.py` -- 17 of the
  18 errors, since every module transitively importing them failed too.
- `tomllib` in `tests/ops/test_rate_web_playwright_workflow.py` -- the 18th.

Both already have an established repo pattern that these bypassed. Of the 17
`StrEnum` imports in the tree, 15 are correctly version-guarded; only these two
were bare. This is the same defect class as the `from datetime import UTC`
regression fixed earlier on this branch: a 3.11+ feature taken directly instead
of through `shared.python.compatibility`.

Used the exact pattern the other modules use, after getting it wrong once:

    if TYPE_CHECKING:
        from enum import StrEnum
    else:
        from shared.python.compatibility import StrEnum

My first attempt imported the shim unconditionally, which fixed 3.10 but broke
Mypy -- 16 new errors, because the shim's `class StrEnum(str, Enum)` fallback
loses StrEnum member typing, so every `ClubCameraAction` and
`VariationVisualEvent` member degraded to `str`. The `TYPE_CHECKING` guard gives
the checker the real `enum.StrEnum` and the 3.10 runtime the shim, which is
precisely why the other modules are written that way.

`tomllib` uses the `try/except ImportError -> tomli` fallback already present in
three other test modules.

Also extended `test_python_310_datetime_boundaries.py`, which previously guarded
only `datetime.UTC`, with an AST check for module-scope `enum.StrEnum` plus a
self-test proving it accepts the guarded and shimmed forms and rejects only the
bare one. An unguarded import here is not a style question: it fails collection
and hides every other result on the lane.

Verified on a real Python 3.10.20 interpreter: `club_camera` imports and resolves
StrEnum from the shim. Mypy back to `Success: no issues found in 374 source
files`; 20 tests pass on 3.11; ruff check and format clean.

Left alone deliberately: `tests/test_pendulum_provider_manifest.py` and
`tests/video_analyzer/test_video_analyzer_packaging.py` also import `tomllib`
unguarded, but neither is in this branch's changed set. Fixing them would pull
them into CI's changed-test selection and could surface unrelated failures for no
benefit here.
dieterolson added a commit that referenced this pull request Aug 15, 2026
…ta CI (#4480)

Three tests were failing on `main` itself, invisible because Tools selects tests from changed files —
so `main`'s own delta CI never put these files in its changed set. Any branch that merely touched them
inherited the failures and appeared to have caused them.

- `test_each_downstream_declares_its_required_sparse_scope`
- `test_upstream_scope_includes_every_release_build_package_root`
- `test_refresh_user_functions_system_exit_propagates`

Fixes, per side-of-truth rather than to make the mirror agree:

- **Workflow was wrong -> fixed the workflow.** `src/chat`, `src/contracts.py`, `src/python/src/utils` and
  `src/sidekick` exist in neither form on UpstreamDrift's default branch (verified via the API, not a local
  clone). Removed rather than replaced with a guess — a test asserting a nonexistent path is a vacuous gate
  (see #4477).
- **Test expectation was stale -> fixed the test.** `REQUIRED_SPARSE_PATHS` becomes
  `{src/shared, tests/shared_contracts, tests/support}`, and the release-build-roots assertion becomes
  `{"src/shared"} <= scope`. Deliberately NOT bare `src`: the test asserts `"src" not in upstream_scope`,
  so narrowness is a design constraint, not drift. Declaring `src` would have satisfied the mirror while
  violating an explicit assertion.
- **`raise SystemExit(1)`** replaces `import sys; sys.exit(1)` in the rotation-converter test. The scripting
  sandbox now blocks `import sys`, so the old form never reached `sys.exit` and `SystemExit` was never
  raised — a stale test defeated by a correct security change.

Verified: 15 passed across both files on 3.11 (was 3 failed); ruff check and format clean; assertion gate
passes; YAML valid; all three files keep their existing line endings, so the CRLF workflow file shows a
content-only diff (see #4479 for the underlying `.gitattributes` gap).

Reviewer context: the Cross-Repo workflow's last *completed* run on `main` was a failure (2026-08-06).
This PR makes the *declaration* honest; it does not claim the job is green end to end. `vendor/ud-tools`
and the submodule-init step are owned by #4447 and deliberately excluded. `tests/fixtures` does exist and
is seeded onto `sys.path` by UpstreamDrift's vendoring test — flagged as a possible further gap, not folded
in here.

Unblocks #4447 and #4446, which inherited these failures by touching the same files.
Filed as #4481.
codex-scheduled and others added 4 commits August 14, 2026 19:20
#4480 landed the three fixes for tests that failed on `main` but were never
selected by its delta CI. Re-syncing clears the last known cause of this
branch's `tests (3.11)` failure.

One conflict, in the release-build-package-root assertion, resolved as the
union: #4480's corrected `{"src/shared"}` plus this branch's
`vendor/ud-tools`, which is the pinned Tools gitlink UpstreamDrift's own
test_tools_vendoring.py resolves against.

I checked the auto-merged regions on content rather than trusting them, since
this is the exact file pair I mis-resolved two merges ago. Both are correct:
the workflow's UpstreamDrift scope is #4480's cleaned list plus
`vendor/ud-tools` with the submodule-init step intact, and
REQUIRED_SPARSE_PATHS carries the same four entries -- so the mirror and the
workflow agree, which is the property the test exists to enforce.

Verified: the three formerly-failing tests now pass (16 passed on 3.11), mypy
clean over CI's 374 files, ruff check and format clean over 584, changed-test
assertion and module-size gates pass, no conflict markers, no gitlinks.
Resolves the single SPEC.md conflict as a union of two independent
hot-prepends into the section 12 version table: main's #4463 firmware row
(2026-08-14, 1.5.8) placed newest-first above this branch's 176 rows, none
of which main carries. Main's two section 3 firmware entries auto-merged.
Zero new duplicate rows introduced; the 3 pre-existing duplicates from the
34-PR fold are unchanged. No other file overlapped, so main's firmware tree
and CI gate merge in unmodified.
@dieterolson

Copy link
Copy Markdown
Collaborator Author

Collision warning: two consolidations of the same campaign, both armed to auto-merge

#4447 (consolidated/variation-morris-2026-08-13) and #4473 (codex/4433-consolidated-release) both consolidate the #4142/#4433 campaign. Both currently have auto-merge armed. Measured against each branch's own merge-base with main:

files
#4447 changed 1043
#4473 changed 1068
shared paths 970
shared paths, byte-identical 802
shared paths, content differs 168
only in #4447 73 (66 added, 7 modified)
only in #4473 98 (all additions, none present on main)

Neither branch subsumes the other, so merging one and closing the other loses work in both directions. This is the same trap measured earlier in this drive, where 26 of 53 non-tip PRs held commits absent from every descendant tip — a later, larger consolidation is not automatically a superset.

What each side uniquely holds

#4473's 98 unique files are pure additions — absent from main and from #4447 — so they cannot clobber anything and are mechanically safe to port. They are all under src/rate_of_closure (73) and tests/rate_of_closure (23), plus one tests/ops and one doc.

#4447's 73 unique files include work that #4473 states it deliberately excluded. #4473's description says it "excludes inherited unrelated repository-wide formatter churn, Movement Optimizer/pendulum/Sidekick work". That exclusion is not only churn — the excluded set contains substantive changes:

  • src/shared/python/swing_sim/variation/execution_metadata.py and its test — new files, and this is variation work directly in scope for Epic: Ensemble variation, swing quiet zones, and sensitivity attribution #4142
  • src/rotation_converter/ui/pyqt6/main_window.py — +25/-122, a real refactor
  • src/pendulum_simulator/tests/test_function_generator_dialog.py +27, test_torque_utils.py +11 — new tests
  • src/shared/python/signal_toolkit/polynomial_generator.py +20/-4
  • scripts/benchmark_rate_ensemble_archive.py, .gitignore

The 168 divergent files are the real problem

802 shared files are byte-identical, so the two branches largely agree. The 168 that differ (125 under src/rate_of_closure, 15 under src/shared, 14 under tests/rate_of_closure, plus SPEC.md, pyproject.toml, AGENT_HANDOFF.md and 4 workflow files) are where they disagree, and that cannot be resolved by a union — each needs a decision about which version is correct.

Recommendation

Pick one branch as the vehicle and port the other's unique content into it, rather than merging both:

No action has been taken on either branch. #4447 is deliberately being held unpushed so it cannot land and strand #4473's additions.

…nder the size budget

Merges origin/main (now carrying #4448 and #4487) into the 34-PR variation /
Morris consolidation, and clears the two gates that were failing locally.

SPEC.md was the only conflict: one hunk in the newest-first Change Log table.
Resolved ours-then-theirs. Both sides insert 2026-08-13 rows, so the date does
not decide the order -- what decides it is that main's row is 1.5.7 and the row
immediately AFTER the conflict is 1.5.6. That adjacency is main's own version
sequence, so main's row has to stay last or the merge silently reorders main's
changelog. Verified after resolving: main's 1.5.8 and 1.5.7 rows both present,
1.5.7 still immediately above the 2026-08-13 1.5.6 rows.

Note this is the OPPOSITE order from the sibling consolidations' SPEC conflicts,
which were theirs-then-ours. Same underlying rule in both cases -- the trailing
side belongs with whatever follows the conflict -- but it resolves to a different
order depending on whether that trailing content is a table row or a section
bullet. Neither can be settled by a blanket "prefer ours" or `--union`.

Formatting: 4 files under src/data_processing/data_processor/python/ were still
carrying the older style this stack had reverted. Reformatted with the CI-pinned
ruff, so `ruff format --check` is clean across all 2976 files.

File-size budget: 2 violations, handled differently on purpose.

- src/shared/python/swing_sim/variation/execution_metadata.py (525 LOC) is SPLIT,
  not grandfathered. Its schema identifiers and field frozensets are declarations
  with no behaviour, so they moved cleanly into
  `_execution_metadata_schema.py` (75 LOC) and are re-exported, leaving every
  existing importer unaffected. 525 -> 488 LOC. Verified all 15 moved names still
  resolve through `execution_metadata` after `ruff --fix` ran over the re-export
  block, since an import-and-re-export pattern is exactly what an unused-import
  autofix is liable to strip.
- src/rate_of_closure/ui/pyqt6/torque_profile_panel.py (612 LOC) IS grandfathered,
  with the weaker justification stated plainly in the baseline file rather than
  glossed: unlike the #4448 entries, this is a NEW file authored over the budget.
  ~503 of its 612 lines are a single QWidget subclass; the only clean seam is
  `TorquePolynomialDialog` (~33 lines), and extracting it still leaves 579. Going
  under 500 means carving up one Qt widget class, moving signal/slot wiring and
  layout construction across a module boundary for a line count. Tracked in #4503.

Verified: ruff check and format clean, size budget 0 violations, zero deletions
relative to main, zero gitlinks in the index.

One pre-existing test failure is untouched and is NOT caused by this merge:
tests/rate_of_closure/test_variation_ensemble_io_reader.py::
test_text_reader_normalizes_decoder_resource_errors fails identically with these
changes stashed, so it predates them.

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