From bf4534b80f6ffb747fd894167ba3c11f7b10a41a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:29:54 +0000 Subject: [PATCH 1/4] Fix macOS wheel build: set MACOSX_DEPLOYMENT_TARGET=10.14 cibuildwheel defaults the x86_64 macOS deployment target to 10.9, but nanobind's C++17 runtime uses aligned new/delete which requires macOS 10.13+. Every macOS wheel job failed to compile nanobind (nb_type.cpp: 'aligned deallocation function ... only available on macOS 10.13 or newer'). Pin the deployment target to 10.14 for the macOS builds. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 41eb3cc9..0cb07840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,9 @@ test-command = 'python -c "import nns, nns._nnscore as c; print(c.lpm(2.0, 0.0, [tool.cibuildwheel.macos] archs = ["x86_64", "arm64"] +# nanobind's C++17 runtime uses aligned new/delete, which requires macOS 10.13+. +# cibuildwheel otherwise defaults x86_64 to 10.9 and the build fails to compile. +environment = { MACOSX_DEPLOYMENT_TARGET = "10.14" } [tool.cibuildwheel.windows] archs = ["AMD64"] From 89442bdd64a950bab2556b34fd09db37278a37f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:50:46 +0000 Subject: [PATCH 2/4] Fix Linux wheel test: build on manylinux_2_28 cibuildwheel's default Linux image is manylinux2014 (glibc 2.17), but numpy/scipy only publish manylinux_2_27/_2_28 wheels. In the glibc 2.17 test container those wheels are incompatible, so pip falls back to compiling scipy from source, which needs OpenBLAS/Fortran that aren't present, and the wheel smoke-test fails. Our runtime deps already require glibc >= 2.27, so manylinux2014 wheels were never installable anyway. Build and test the x86_64 Linux wheels on manylinux_2_28 to match the dependencies' wheel floor. musllinux_1_2 (the cibuildwheel default) is pinned explicitly; it already has matching numpy/scipy wheels. --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0cb07840..588f2e88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,12 @@ mypy_path = ["tests"] build = "cp311-* cp312-* cp313-*" skip = ["pp*", "*_i686", "*-win32", "*-musllinux_i686"] build-frontend = "build" +# Build/test on glibc 2.28: numpy/scipy only publish manylinux_2_27/_2_28 wheels, +# so the default manylinux2014 (glibc 2.17) test image cannot install them and +# falls back to compiling scipy from source (no OpenBLAS) and fails. musllinux_1_2 +# (the cibuildwheel default) already has matching numpy/scipy wheels. +manylinux-x86_64-image = "manylinux_2_28" +musllinux-x86_64-image = "musllinux_1_2" # Smoke-test every built wheel: the native extension imports and computes. test-command = 'python -c "import nns, nns._nnscore as c; print(c.lpm(2.0, 0.0, [-2.0, -1.0, 0.5, 3.0]))"' From 71916a6a5fa01ca6c9dd2548970bb5abdfc90fa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:12:03 +0000 Subject: [PATCH 3/4] Record release provenance: resolve r_commit and core_commit Resolve the two placeholder SHAs in sync/nns_source.json by matching the vendored bytes to upstream history: - r_commit 905b8bbd: the OVVO-Financial/NNS commit that introduced the exact vendored tools/NNS_13.0.tar.gz blob (parent carried a different tarball). Version 13.0 / Date 2026-06-10, Packaged 2026-06-11 03:14 UTC. - core_commit 7f93df9d: the OVVO-Financial/NNS-core commit that authored the exact vendored extern/NNS-core include/src/CMakeLists objects (its parent had different include/src). A later commit carries the identical core forward unchanged. The release provenance gate now passes for a real tagged release. test_unknown_provenance_fails_real_release previously ran the gate against the committed manifest and relied on it carrying placeholder provenance. Point it at a fixture manifest so it tests the unknown-provenance failure behavior without depending on the real manifest being unfilled. --- sync/nns_source.json | 4 ++-- tests/tools/test_release_provenance.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/sync/nns_source.json b/sync/nns_source.json index a592d4fb..92867f1c 100644 --- a/sync/nns_source.json +++ b/sync/nns_source.json @@ -1,10 +1,10 @@ { "r_repo": "OVVO-Financial/NNS", - "r_commit": "unknown", + "r_commit": "905b8bbd42b3236bf88aba7f18df7a9a378dbd7b", "r_version": "13.0", "r_src_tree_hash": "654e411bd4e8caabfd57a1a4190eb1d97411e059", "core_repo": "OVVO-Financial/NNS-core", - "core_commit": "unknown", + "core_commit": "7f93df9dff8762df870c1fbba6e03c0469be6e69", "python_repo": "OVVO-Financial/NNS-python", "python_commit": null, "vendored_core_path": "extern/NNS-core", diff --git a/tests/tools/test_release_provenance.py b/tests/tools/test_release_provenance.py index 4cc39e2b..f32a0ae1 100644 --- a/tests/tools/test_release_provenance.py +++ b/tests/tools/test_release_provenance.py @@ -43,8 +43,10 @@ def test_allow_unknown_passes_with_placeholder_provenance() -> None: assert result.returncode == 0, result.stdout + result.stderr -def test_unknown_provenance_fails_real_release() -> None: - result = _run([]) +def test_unknown_provenance_fails_real_release(tmp_path: Path) -> None: + manifest = tmp_path / "manifest.json" + _write_manifest(manifest) # default r_commit/core_commit are "unknown" + result = _run(["--manifest", str(manifest)]) assert result.returncode != 0 assert "provenance" in (result.stdout + result.stderr).lower() From f73a3cc2e5fa3735d8e1a6ec0151472584cdb830 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 15:15:23 +0000 Subject: [PATCH 4/4] Finalize version 1.0.0 Drop the pre-release suffix (1.0.0a0 -> 1.0.0) and update the trove classifier from Development Status :: 3 - Alpha to 5 - Production/Stable. Bump the hardcoded nns.__version__ to match; nothing enforced parity between it and pyproject before, so add tests/tools/test_version_sync.py to assert nns.__version__ equals the pyproject project.version. twine check --strict passes on the 1.0.0 sdist. --- pyproject.toml | 4 ++-- src/nns/__init__.py | 2 +- tests/tools/test_version_sync.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 tests/tools/test_version_sync.py diff --git a/pyproject.toml b/pyproject.toml index 588f2e88..7efb2a63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ovvo-nns" -version = "1.0.0a0" +version = "1.0.0" description = "Python port of nonlinear nonparametric statistics from R NNS" readme = "README.md" requires-python = ">=3.11" @@ -14,7 +14,7 @@ maintainers = [ { name = "Fred Viole", email = "ovvo.open.source@gmail.com" }, ] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", diff --git a/src/nns/__init__.py b/src/nns/__init__.py index e6cc0fdb..de784844 100644 --- a/src/nns/__init__.py +++ b/src/nns/__init__.py @@ -4,7 +4,7 @@ from nns.pm_matrix import pm_matrix as pm_matrix -__version__ = "1.0.0a0" +__version__ = "1.0.0" _EXPORTS = { "FactorDesign": ("nns.regression", "FactorDesign"), diff --git a/tests/tools/test_version_sync.py b/tests/tools/test_version_sync.py new file mode 100644 index 00000000..e1e93ab4 --- /dev/null +++ b/tests/tools/test_version_sync.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import re +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _pyproject_version() -> str: + data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + return str(data["project"]["version"]) + + +def _package_version() -> str: + text = (REPO_ROOT / "src" / "nns" / "__init__.py").read_text(encoding="utf-8") + match = re.search(r'^__version__\s*=\s*"([^"]+)"', text, re.MULTILINE) + assert match is not None, "could not find __version__ in src/nns/__init__.py" + return match.group(1) + + +def test_package_version_matches_pyproject() -> None: + assert _package_version() == _pyproject_version()