diff --git a/.context/eegprep_lean_contract.md b/.context/eegprep_lean_contract.md index 28e6dcc2a..de2f5a748 100644 --- a/.context/eegprep_lean_contract.md +++ b/.context/eegprep_lean_contract.md @@ -60,13 +60,19 @@ rather than as a snippet in a string. `eegprep-lean` targets Pyodide. Pyodide 0.29.5 bundles CPython 3.13.2 on `emscripten_4_0_9`, and a browser cannot be told to run a different one. -So `requires-python` and every dependency floor are a **ceiling** here, not a floor: -a floor above the version Pyodide bundles cannot be satisfied, because micropip has no -WebAssembly build to fall back to. +So `requires-python`, and the floor of every **compiled** dependency, are a **ceiling** here rather +than a floor: PyPI carries no WebAssembly build, so Pyodide's bundled one is the only one a browser +can have and a floor above it cannot be satisfied. + +The test is whether the package publishes a pure-Python wheel, not whether Pyodide bundles it. +A `py3-none-any` wheel means micropip fetches any version straight from PyPI and the bundled build +goes unused, so that floor costs a download and nothing more. Of the packages that matter here, +numpy, scipy, h5py and matplotlib publish none and are genuinely capped; threadpoolctl is bundled +but pure Python and is not. This inverts the usual reading and it is invisible from the code, which is why -eegprep's `tests/test_browser_dependency_floors.py` (sccn/eegprep#399) enforces it, and why -`eegprep-lean` inherits the same test. +eegprep's `tests/test_browser_dependency_floors.py` (sccn/eegprep#399, corrected in #402) enforces +it for the compiled set, and why `eegprep-lean` inherits the same test. ### Numerical agreement with eegprep diff --git a/.memory/pyodide-is-a-ceiling-not-a-floor.md b/.memory/pyodide-is-a-ceiling-not-a-floor.md index b39fd41b3..cd7531be8 100644 --- a/.memory/pyodide-is-a-ceiling-not-a-floor.md +++ b/.memory/pyodide-is-a-ceiling-not-a-floor.md @@ -5,30 +5,40 @@ metadata: type: project --- -Pyodide bundles its own build of every compiled scientific package, and micropip has no -WebAssembly build to fall back to. So for anything that has to run in the browser, -`requires-python` and every dependency floor act as a **ceiling**: a floor above the version a -Pyodide release ships cannot be satisfied there, no matter how reasonable the bump looked. +For anything that has to run in the browser, `requires-python` and the floors of **compiled** +dependencies act as a **ceiling**: Pyodide bundles its own WebAssembly build, PyPI has none, so a +floor above the bundled version cannot be satisfied there no matter how reasonable the bump looked. -This inverts the usual reading of a floor and is invisible from the code, which is how -`threadpoolctl>=3.6.0` reached `epic/324-pyodide-browser` against a distribution shipping 3.5.0. +**The test is whether the package publishes a pure-Python wheel, not whether Pyodide bundles it.** +A `py3-none-any` wheel means micropip can fetch any version straight from PyPI and the bundled +build is simply unused, so the floor costs a download and nothing more. -Verified 2026-09-20 against `https://cdn.jsdelivr.net/pyodide/v0.29.5/full/pyodide-lock.json`: +Verified 2026-09-20 against `https://cdn.jsdelivr.net/pyodide/v0.29.5/full/pyodide-lock.json` and +PyPI: -| | Pyodide 0.29.5 | -|---|---| -| Python | 3.13.2 (`emscripten_4_0_9`) | -| threadpoolctl | 3.5.0 | -| scipy | 1.14.1 | -| numpy | 2.2.5 | -| matplotlib | 3.8.4 | -| h5py | 3.13.0 | +| | Pyodide 0.29.5 | universal wheel on PyPI | floor is a ceiling | +|---|---|---|---| +| Python | 3.13.2 (`emscripten_4_0_9`) | n/a | yes | +| scipy | 1.14.1 | none | yes | +| numpy | 2.2.5 | none | yes | +| matplotlib | 3.8.4 | none | yes | +| h5py | 3.13.0 | none | yes | +| threadpoolctl | 3.5.0 | `py3-none-any` | **no** | + +I originally wrote this entry citing `threadpoolctl>=3.6.0` on `epic/324-pyodide-browser` as a live +broken install. **That was wrong**, and it is the instructive part: the package is bundled by +Pyodide, which made it look identical to the scipy case, but it is pure Python and micropip +installs 3.6.0 from PyPI without complaint. Being in `pyodide-lock.json` says nothing about whether +a different version is reachable. **Why it matters:** the failure is an install that cannot resolve in the browser, found by a user -rather than by CI, and the bump that caused it will look routine in review. +rather than by CI, and the bump that caused it will look routine in review. Over-applying the rule +is its own cost: it produces false alarms on pure-Python packages and a guard nobody trusts. **How to apply:** read the versions out of that release's `pyodide-lock.json` before raising any -floor on a browser-bound package, and never from a changelog. eegprep enforces this offline in +floor on a browser-bound package, and never from a changelog. Then check PyPI for a +`py3-none-any` wheel at the version you want, and only treat the floor as a ceiling when there is +none. eegprep enforces this offline in `tests/test_browser_dependency_floors.py` (sccn/eegprep#399); copy that shape rather than trusting review. Note that markers are evaluated with `sys_platform == "emscripten"`, so a `sys_platform != 'darwin'` branch is the one the browser takes. Do not trust