diff --git a/CHANGELOG.md b/CHANGELOG.md index 66cb38557..100890984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -279,6 +279,11 @@ - Added sentinel delegation tests, Rust edge-case tests, seeded true-trait recovery tests, score-equation checks, and APA 7th CAT doctoring. +#### Automatic backend preserves Rust numerical ownership + +- Changed `backend="auto"` so a missing compiled Rust core fails closed instead of silently selecting the independent NumPy reference implementation. +- Kept explicit `backend="numpy"` as an explicit reference/parity choice while preserving automatic Rust resolution and Rust CPU/GPU device fallback semantics. + #### Current support-policy version line - Aligned the public security and support policies with the released `0.7.x` pre-1.0 package line instead of the obsolete `0.1.x` policy. @@ -312,6 +317,11 @@ ### Fixed +#### Serving bundle export requires Rust core + +- `export_serving_bundle` fails closed when the compiled Rust core is unavailable + instead of shipping incomplete bundles with null `eapsum_tables`. + #### Fail early for unimplemented estimator identities - Restricted the public `FitConfig.estimator` vocabulary to the implemented `jmle` and `mmle` fitting paths, so unsupported `em` and `bayes` requests fail during configuration validation instead of entering a fitting path that later raises `NotImplementedError`. @@ -331,6 +341,10 @@ - Validate automated-test-assembly content-label shape and string element types before item-information evaluation, rejecting arbitrary object labels without invoking caller-controlled `__str__`/`__repr__` callbacks while preserving accepted Python/NumPy string labels and existing assembly numerics. +#### Bound PR queue Git metadata lookup + +- PR queue governance now bounds the `git rev-parse HEAD` subprocess and fails closed with a stable timeout error instead of allowing a hung local Git child to stall the evidence pipeline. + #### Diagnostics-report focus and contrast preservation - Revealed the visually hidden diagnostics-report skip link for every actual `:focus` state while retaining the explicit `:focus-visible` treatment and strong outline. @@ -343,6 +357,10 @@ - Stopped reflecting caller-controlled invalid response values in `person_fit_np()` validation errors while preserving the failing matrix coordinate and the complete-data 0/1 response contract. +#### Bounded mixed item-model validation + +- Bounded `item_models` iterable consumption to at most one look-ahead entry beyond the calibrated item count, rejected arbitrary non-string model controls before caller `__str__`/`__repr__` hooks can execute, and removed rejected model content from public validation errors while preserving accepted aliases and Rust-owned calibration numerics. + #### Model-comparison control validation hardening - Reject hostile semantic/numeric control objects before caller-defined `__str__`, `__repr__`, or `__float__` callbacks can execute, while preserving accepted relation identities, built-in/NumPy scalar semantics, and Rust-owned model-comparison arithmetic. diff --git a/docs/changelog.d/707-pr-queue-git-metadata-timeout.md b/docs/changelog.d/707-pr-queue-git-metadata-timeout.md new file mode 100644 index 000000000..4bc55f278 --- /dev/null +++ b/docs/changelog.d/707-pr-queue-git-metadata-timeout.md @@ -0,0 +1,5 @@ +# Bound PR queue Git metadata lookup + +## Fixed + +- PR queue governance now bounds the `git rev-parse HEAD` subprocess and fails closed with a stable timeout error instead of allowing a hung local Git child to stall the evidence pipeline. diff --git a/scripts/build_pr_queue_governance.py b/scripts/build_pr_queue_governance.py index 7c6234b20..65b8b3b9e 100644 --- a/scripts/build_pr_queue_governance.py +++ b/scripts/build_pr_queue_governance.py @@ -86,6 +86,7 @@ _GH_TRANSIENT_STATUS_RE = re.compile(r"\bHTTP (?:502|503|504)\b", re.IGNORECASE) _GH_JSON_MAX_ATTEMPTS = 3 _GH_JSON_RETRY_SLEEP_SECONDS = 0.5 +GIT_METADATA_TIMEOUT_SECONDS = 5 def _parse_datetime(value: str) -> datetime: @@ -122,7 +123,7 @@ def _resolve_path(value: str | Path, *, base: Path) -> Path: def _source_commit(repo_root: Path) -> str: - """Return the checked-out commit SHA, or ``unknown`` when unavailable.""" + """Return the checked-out commit SHA, failing closed on a hung Git child.""" try: completed = subprocess.run( ["git", "rev-parse", "HEAD"], @@ -130,7 +131,10 @@ def _source_commit(repo_root: Path) -> str: capture_output=True, text=True, check=True, + timeout=GIT_METADATA_TIMEOUT_SECONDS, ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("source commit lookup timed out") from exc except Exception: return "unknown" return completed.stdout.strip() or "unknown" diff --git a/tests/test_pr_queue_governance_git_timeout.py b/tests/test_pr_queue_governance_git_timeout.py new file mode 100644 index 000000000..c3dffb259 --- /dev/null +++ b/tests/test_pr_queue_governance_git_timeout.py @@ -0,0 +1,40 @@ +"""Fail-first reliability contracts for PR queue governance Git metadata reads.""" + +from __future__ import annotations + +import importlib.util +import subprocess +from pathlib import Path + +import pytest + + +def _load_governance(): + """Load the PR queue governance script as a module for boundary tests.""" + script = Path(__file__).resolve().parents[1] / "scripts" / "build_pr_queue_governance.py" + spec = importlib.util.spec_from_file_location("build_pr_queue_governance", script) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_source_commit_bounds_git_metadata_lookup(monkeypatch, tmp_path: Path) -> None: + """A hung ``git rev-parse`` must fail closed under a package-owned deadline.""" + module = _load_governance() + observed_timeouts: list[object] = [] + + def timeout_run(*args, **kwargs): + observed_timeouts.append(kwargs.get("timeout")) + raise subprocess.TimeoutExpired( + cmd=args[0] if args else kwargs.get("args", ["git", "rev-parse", "HEAD"]), + timeout=kwargs.get("timeout"), + ) + + monkeypatch.setattr(module.subprocess, "run", timeout_run) + + with pytest.raises(RuntimeError, match="source commit lookup timed out"): + module._source_commit(tmp_path) + + assert observed_timeouts == [module.GIT_METADATA_TIMEOUT_SECONDS]