diff --git a/CLAUDE.md b/CLAUDE.md index fce4fa905..927d57938 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,21 @@ fully before making changes. In particular it defines: deployment concerns. The hosted runtime must not be recreated under `services/assessment_runtime` in this repository. +## Runtime Contract + +The following block is machine-checked against package metadata and the shipped +backend-ownership policy. Keep it as one exact TOML block rather than replacing +it with an equivalent prose-only claim. + + +```toml +[runtime_contract] +requires_python = ">=3.12" +auto_backend = "rust_required" +numpy_role = "reference_parity_only" +``` + + ## Common Commands ### Setup @@ -35,9 +50,10 @@ toolchain should be on `PATH` (`cargo`/`rustc`) for deterministic local builds. If cargo is absent, maturin may try to provision a temporary Rust toolchain via `puccinialin`; set `MATURIN_NO_INSTALL_RUST=1` when you need a fail-fast offline/proxy-safe build. A proxy or certificate error in that fallback is not -proof of a Python/PyO3 incompatibility. `pyproject.toml` declares Python -`>=3.10`; required CI currently builds and tests CPython 3.12, so broader -interpreter claims need matching build/import/full-suite CI evidence. +proof of a Python/PyO3 incompatibility. `pyproject.toml` declares +`requires-python = ">=3.12"`, matching the required CPython 3.12 and 3.14 CI +matrix. Broader interpreter claims need matching build/import/full-suite CI +evidence. ```bash python -m pip install -e . # builds fast_mlsirm._core via maturin @@ -145,9 +161,11 @@ examples/enterprise_demo/ Synthetic procurement evidence manifests PyO3/numpy. maturin (configured in `pyproject.toml`) compiles the extension into `fast_mlsirm._core` during install. - The **backend axis is `{numpy, rust, auto}`** (default `auto`), resolved in - `python/fast_mlsirm/backend.py`: Rust when `_core` imports, otherwise the - numerically-identical NumPy reference in `python/fast_mlsirm/objective.py` - and `math.py`, which is kept for parity testing and fallback. + `python/fast_mlsirm/backend.py`: explicit `numpy` is the reference/parity + choice, explicit `rust` requires the compiled core, and production-convenience + `auto` resolves to Rust when `_core` imports. `auto` fails closed when the + compiled Rust core is unavailable; automatic resolution never silently + changes the numerical owner to NumPy. - **GPU is a device sub-option of the Rust backend**, not a separate backend: `FitConfig(backend="rust", rust_device={auto,cpu,gpu})`. The wgpu kernels in `crates/mlsirm-core/src/gpu.rs` run in f32 and fall back to the f64 scalar diff --git a/tests/test_claude_runtime_contract.py b/tests/test_claude_runtime_contract.py new file mode 100644 index 000000000..d02b51cfa --- /dev/null +++ b/tests/test_claude_runtime_contract.py @@ -0,0 +1,57 @@ +"""Prevent canonical agent guidance from drifting from shipped runtime policy.""" + +from __future__ import annotations + +from pathlib import Path +import re +import tomllib + + +ROOT = Path(__file__).resolve().parents[1] +_RUNTIME_CONTRACT_START = "" +_RUNTIME_CONTRACT_END = "" + + +def _runtime_contract(guidance: str) -> dict[str, str]: + """Return the single machine-readable runtime contract from agent guidance.""" + assert guidance.count(_RUNTIME_CONTRACT_START) == 1 + assert guidance.count(_RUNTIME_CONTRACT_END) == 1 + payload = guidance.split(_RUNTIME_CONTRACT_START, 1)[1].split( + _RUNTIME_CONTRACT_END, 1 + )[0] + match = re.fullmatch(r"\s*```toml\s*(.*?)\s*```\s*", payload, re.DOTALL) + assert match is not None + contract = tomllib.loads(match.group(1))["runtime_contract"] + assert all(type(value) is str for value in contract.values()) + return contract + + +def test_claude_machine_runtime_contract_matches_shipped_policy() -> None: + """Canonical machine guidance must pin package and backend ownership policy.""" + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + guidance = (ROOT / "CLAUDE.md").read_text(encoding="utf-8") + + assert _runtime_contract(guidance) == { + "requires_python": project["project"]["requires-python"], + "auto_backend": "rust_required", + "numpy_role": "reference_parity_only", + } + + +def test_claude_python_floor_matches_package_metadata() -> None: + """Agent guidance must advertise the exact supported Python floor.""" + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + requires_python = project["project"]["requires-python"] + guidance = (ROOT / "CLAUDE.md").read_text(encoding="utf-8") + + assert f'`requires-python = "{requires_python}"`' in guidance + assert "`>=3.10`" not in guidance + + +def test_claude_auto_backend_matches_fail_closed_runtime() -> None: + """Canonical guidance must not advertise a silent NumPy production fallback.""" + guidance = (ROOT / "CLAUDE.md").read_text(encoding="utf-8") + normalized = " ".join(guidance.split()) + + assert "`auto` fails closed when the compiled Rust core is unavailable" in normalized + assert "otherwise the numerically-identical NumPy reference" not in normalized