diff --git a/.github/releases/v0.2.3.md b/.github/releases/v0.2.3.md
new file mode 100644
index 000000000..cb9b9a94f
--- /dev/null
+++ b/.github/releases/v0.2.3.md
@@ -0,0 +1,155 @@
+# statgpu 0.2.3
+
+statgpu 0.2.3 is a substantial survival-analysis release. It completes the first major Cox proportional-hazards implementation phase, expands Cox model selection and penalized estimation, and hardens numerical, inference, and packaging behavior across the supported NumPy, CuPy, and PyTorch backends.
+
+## Highlights
+
+- Complete CoxPH support for **Breslow, Efron, and Exact ties**.
+- Delayed entry and `(start, stop]` counting-process data.
+- Shared-coefficient stratified Cox models with stratum-specific baselines.
+- Subject-aware repeated-row handling and `Surv(start, stop, event)` formula input.
+- Extended `CoxPHCV` scoring, fold construction, diagnostics, and final refitting.
+- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation.
+- Universal `py3-none-any` wheel validated on Linux, Windows, and macOS.
+
+## CoxPH Phase 1 completion
+
+### Tie handling and risk sets
+
+`CoxPH` now supports all three primary tie methods:
+
+- `ties="breslow"`;
+- `ties="efron"`;
+- `ties="exact"`.
+
+The implementation uses shared NumPy, CuPy, and Torch-CUDA risk-set primitives for the partial-likelihood objective, gradient, information matrix, and baseline estimation. Exact tied-event partitions use backend-native dynamic programming rather than a CPU-only implementation.
+
+### Delayed entry, start-stop rows, and stratification
+
+The public Cox interface now supports:
+
+- delayed entry;
+- `(start, stop]` counting-process rows;
+- repeated rows belonging to the same subject;
+- shared coefficients with stratum-specific baseline hazards;
+- formula input through `Surv(start, stop, event)`.
+
+Formula-driven NA removal now keeps entry, cluster, strata, subject, response, and design arrays aligned.
+
+### Inference and baseline prediction
+
+For Breslow and Efron fits, model-based, HC0, HC1, and cluster covariance are supported where the requested data configuration is eligible. Exact ties currently support model-based covariance only; unsupported robust-covariance requests fail explicitly instead of silently changing behavior.
+
+Cox numerical stability has been strengthened through centered risk-set moments and log-domain baseline prediction. Singular information matrices are rejected rather than returning misleading zero standard errors.
+
+Baseline prediction requires `compute_inference=True`. The conventional Breslow baseline estimator is used after coefficient fitting, including when coefficients were fitted with Efron or Exact ties.
+
+## CoxPHCV completion
+
+`CoxPHCV` held-out partial likelihood now supports:
+
+- Breslow, Efron, and Exact ties;
+- delayed entry and start-stop data;
+- strata;
+- subject-grouped folds, so repeated rows from one subject remain in one fold;
+- device-native held-out scoring;
+- convergence- and failure-aware candidate diagnostics;
+- selected-penalty refitting on the complete dataset.
+
+Full-data cache identities, fold validation, cloneability, repeated fitting, pickling, and failed-refit state cleanup were hardened.
+
+Requested two-stage or successive-halving controls currently execute one deterministic exhaustive full-precision candidate pass. This avoids repeated complete-grid fitting without pretending that unsafe screening has occurred.
+
+## Penalized Cox and grouped penalties
+
+Penalized Cox estimation was hardened for:
+
+- L1;
+- L2;
+- Elastic Net;
+- SCAD;
+- MCP.
+
+The unidentified Cox intercept was removed, Cox-specific SCAD/MCP warm starts were corrected, and Torch Efron value, gradient, and Hessian paths remain native rather than routing through CuPy.
+
+`PenalizedCoxPHModel` remains an estimation-only API in this release. Passing `compute_inference=True` raises `NotImplementedError` explicitly.
+
+Public Group Lasso and Adaptive Group Lasso behavior now follows the generic loss-gradient and exact group-proximal implementation consistently across supported backends.
+
+## Reliability and API hardening
+
+This release also improves:
+
+- failed-fit and failed-refit state resets;
+- singular-information handling;
+- censoring- and tie-correct concordance semantics;
+- device-label grouping without accidental coercion;
+- independence from optional statsmodels for robust Cox covariance;
+- CV cache identity and candidate eligibility;
+- one-shot `cv_splits` iterator reuse across repeated fit, clone, reconstruction, and pickle.
+
+## Installation and platform support
+
+Base CPU installation:
+
+```bash
+pip install statgpu==0.2.3
+```
+
+CUDA extras:
+
+```bash
+pip install "statgpu[gpu11]==0.2.3"
+pip install "statgpu[gpu12]==0.2.3"
+```
+
+PyTorch backend:
+
+```bash
+pip install "statgpu[torch]==0.2.3"
+```
+
+The published wheel is a pure-Python `py3-none-any` artifact. The exact release wheel is clean-installed and exercised on Ubuntu, Windows, and macOS using Python 3.11, including a CPU `LinearRegression.fit/predict` smoke test and public Cox imports. The broader regression suite covers Python 3.9–3.12 on Ubuntu.
+
+The cross-platform wheel validation establishes CPU-wheel portability. It does not add Apple MPS support. CUDA execution still requires a compatible NVIDIA driver/runtime and the matching CuPy or PyTorch package.
+
+Optional Cython CPU accelerators are not embedded in the universal wheel. Their `.pyx`/`.pxd` sources remain in the source distribution for local builds.
+
+## Validation
+
+The final reviewed implementation head for the CoxPH Phase 1 work is:
+
+```text
+f05a44ad363b46612e956e137e2f00d040765acb
+```
+
+Hosted workflow #960 passed documentation, static, full CPU, and Python 3.9–3.12 regression jobs. The complete CPU suite reported **1881 passed and 662 skipped**.
+
+The exact-head physical-GPU promotion artifact records:
+
+- schema 3;
+- 134/134 checks passed;
+- zero child and nested return codes;
+- empty gate-failure arrays;
+- clean source state before and after execution;
+- SHA-256 `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`.
+
+Physical-GPU evidence: https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4
+
+No universal GPU speedup claim is made. Performance depends on problem size, backend, hardware, tie method, and synchronization costs.
+
+## Upgrade notes and known limits
+
+- Python 3.9 or newer is required.
+- Exact ties do not currently provide robust or cluster covariance.
+- `PenalizedCoxPHModel` inference is not implemented in 0.2.3.
+- Baseline prediction requires inference-enabled fitting.
+- Two-stage and successive-halving CoxPHCV controls currently use the documented exhaustive single-pass safety strategy.
+- Apple MPS is not currently a statgpu device backend.
+
+## Full change history
+
+- Main implementation: https://github.com/TheHiddenObserver/statgpu/pull/80
+- Release preparation: https://github.com/TheHiddenObserver/statgpu/pull/86
+- Full comparison: https://github.com/TheHiddenObserver/statgpu/compare/v0.2.2...v0.2.3
+- Repository changelog: https://github.com/TheHiddenObserver/statgpu/blob/master/CHANGELOG.md
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2cdf0f659..805be1800 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -3,13 +3,14 @@ name: Publish to PyPI
on:
push:
tags:
- - 'v*'
+ - "v*"
permissions:
contents: read
jobs:
publish:
+ name: Publish distributions to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -17,37 +18,150 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: "3.11"
- name: Install build tools
run: |
python -m pip install --upgrade pip
python -m pip install build twine
- - name: Verify tag matches package version
+ - name: Verify tag, package versions, and release notes
run: |
- TAG_VERSION=${GITHUB_REF#refs/tags/v}
- PKG_VERSION=$(python -c "import re; print(re.search(r\"version\s*=\s*['\\\"]([^'\\\"]+)['\\\"]\", open('pyproject.toml').read()).group(1))")
- if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
- echo "ERROR: Tag version ($TAG_VERSION) does not match package version ($PKG_VERSION)"
- exit 1
- fi
+ python - <<'PY'
+ import os
+ import pathlib
+ import re
+ import tomllib
+
+ root = pathlib.Path.cwd()
+ tag = os.environ["GITHUB_REF_NAME"]
+ if not tag.startswith("v"):
+ raise SystemExit(f"release tag must start with v: {tag}")
+ tag_version = tag[1:]
+
+ pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
+ project_version = pyproject["project"]["version"]
+ init_text = (root / "statgpu/__init__.py").read_text(encoding="utf-8")
+ match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M)
+ if match is None:
+ raise SystemExit("statgpu/__init__.py does not declare __version__")
+ package_version = match.group(1)
- - name: Build package (pure-Python wheel + sdist)
- # STATGPU_NO_EXT=1 -> no compiled extensions, so the wheel is tagged
- # py3-none-any (universal). The sdist still ships .pyx/.pxd sources for
- # users who want to build the optional C accelerators locally.
+ if len({tag_version, project_version, package_version}) != 1:
+ raise SystemExit(
+ "release version mismatch: "
+ f"tag={tag_version}, pyproject.toml={project_version}, "
+ f"statgpu/__init__.py={package_version}"
+ )
+
+ notes_path = root / ".github" / "releases" / f"{tag}.md"
+ if not notes_path.is_file():
+ raise SystemExit(f"missing GitHub Release notes: {notes_path.relative_to(root)}")
+ notes = notes_path.read_text(encoding="utf-8")
+ if not notes.startswith(f"# statgpu {tag_version}\n"):
+ raise SystemExit(
+ f"GitHub Release notes title does not match tag {tag}: "
+ f"{notes_path.relative_to(root)}"
+ )
+ required_sections = [
+ "## Highlights",
+ "## Installation and platform support",
+ "## Validation",
+ "## Upgrade notes and known limits",
+ "## Full change history",
+ ]
+ missing = [section for section in required_sections if section not in notes]
+ if missing:
+ raise SystemExit(
+ "GitHub Release notes are incomplete: " + ", ".join(missing)
+ )
+ print(f"validated version {tag_version} and {notes_path.relative_to(root)}")
+ PY
+
+ - name: Build package (pure-Python wheel and sdist)
env:
STATGPU_NO_EXT: "1"
- run: python -m build
+ run: |
+ rm -rf build dist *.egg-info statgpu.egg-info
+ python -m build
- name: Check distributions
run: |
python -m twine check dist/*
- ls -l dist/
+ ls -lh dist/
+
+ - name: Smoke-install release wheel
+ run: |
+ WHEEL="$(realpath dist/*.whl)"
+ python -m venv "$RUNNER_TEMP/statgpu-publish-smoke"
+ "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" -m pip install --upgrade pip
+ "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" -m pip install "$WHEEL"
+ cd "$RUNNER_TEMP"
+ "$RUNNER_TEMP/statgpu-publish-smoke/bin/python" - <<'PY'
+ import os
+ import statgpu
+ from statgpu.linear_model import LinearRegression
+ from statgpu.survival import CoxPH, CoxPHCV
+
+ expected = os.environ["GITHUB_REF_NAME"][1:]
+ assert statgpu.__version__ == expected
+ assert LinearRegression is not None
+ assert CoxPH is not None
+ assert CoxPHCV is not None
+ print(statgpu.__version__)
+ PY
+
+ - name: Retain validated release artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: statgpu-tagged-release-distributions
+ path: dist/*
+ if-no-files-found: error
+ retention-days: 14
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: python -m twine upload --non-interactive dist/*
+
+ github-release:
+ name: Publish GitHub Release
+ needs: publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download validated release artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: statgpu-tagged-release-distributions
+ path: dist
+
+ - name: Create or update GitHub Release from versioned notes
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ TAG="$GITHUB_REF_NAME"
+ VERSION="${TAG#v}"
+ NOTES_FILE=".github/releases/${TAG}.md"
+ TITLE="statgpu ${VERSION}"
+
+ test -f "$NOTES_FILE"
+ if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
+ gh release edit "$TAG" \
+ --repo "$GITHUB_REPOSITORY" \
+ --title "$TITLE" \
+ --notes-file "$NOTES_FILE"
+ gh release upload "$TAG" dist/* \
+ --repo "$GITHUB_REPOSITORY" \
+ --clobber
+ else
+ gh release create "$TAG" dist/* \
+ --repo "$GITHUB_REPOSITORY" \
+ --verify-tag \
+ --title "$TITLE" \
+ --notes-file "$NOTES_FILE"
+ fi
diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml
new file mode 100644
index 000000000..5ace632df
--- /dev/null
+++ b/.github/workflows/release-notes.yml
@@ -0,0 +1,118 @@
+name: Release notes validation
+
+on:
+ pull_request:
+ branches: [master]
+ paths:
+ - "pyproject.toml"
+ - "statgpu/__init__.py"
+ - "CHANGELOG.md"
+ - "docs/en/changelog.md"
+ - "docs/cn/changelog.md"
+ - "RELEASING.md"
+ - ".github/releases/**"
+ - ".github/workflows/publish.yml"
+ - ".github/workflows/release-notes.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ validate-release-notes:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Validate versioned GitHub Release notes
+ run: |
+ python - <<'PY'
+ import pathlib
+ import re
+ import tomllib
+
+ root = pathlib.Path.cwd()
+ pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
+ version = pyproject["project"]["version"]
+
+ init_text = (root / "statgpu/__init__.py").read_text(encoding="utf-8")
+ match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M)
+ if match is None:
+ raise SystemExit("statgpu/__init__.py does not declare __version__")
+ if match.group(1) != version:
+ raise SystemExit(
+ f"version mismatch: pyproject.toml={version}, "
+ f"statgpu/__init__.py={match.group(1)}"
+ )
+
+ notes_path = root / ".github" / "releases" / f"v{version}.md"
+ if not notes_path.is_file():
+ raise SystemExit(f"missing GitHub Release notes: {notes_path.relative_to(root)}")
+
+ notes = notes_path.read_text(encoding="utf-8")
+ lines = notes.splitlines()
+ expected_title = f"# statgpu {version}"
+ if not lines or lines[0].strip() != expected_title:
+ raise SystemExit(
+ f"release notes must start with {expected_title!r}: "
+ f"{notes_path.relative_to(root)}"
+ )
+
+ required_sections = [
+ "## Highlights",
+ "## Installation and platform support",
+ "## Validation",
+ "## Upgrade notes and known limits",
+ "## Full change history",
+ ]
+ missing_sections = [section for section in required_sections if section not in notes]
+ if missing_sections:
+ raise SystemExit(
+ "release notes are missing required sections: "
+ + ", ".join(missing_sections)
+ )
+
+ if len(notes.split()) < 500:
+ raise SystemExit("GitHub Release notes are too short to describe the release")
+
+ forbidden_placeholders = ["TODO", "TBD", "X.Y.Z", "CHANGEME"]
+ present_placeholders = [token for token in forbidden_placeholders if token in notes]
+ if present_placeholders:
+ raise SystemExit(
+ "release notes contain unresolved placeholders: "
+ + ", ".join(present_placeholders)
+ )
+
+ required_fragments = [
+ f"pip install statgpu=={version}",
+ f"...v{version}",
+ "Windows",
+ "macOS",
+ "Ubuntu",
+ ]
+ missing_fragments = [fragment for fragment in required_fragments if fragment not in notes]
+ if missing_fragments:
+ raise SystemExit(
+ "release notes are missing user-facing release details: "
+ + ", ".join(missing_fragments)
+ )
+
+ synchronized_files = [
+ root / "CHANGELOG.md",
+ root / "docs" / "en" / "changelog.md",
+ root / "docs" / "cn" / "changelog.md",
+ ]
+ for path in synchronized_files:
+ text = path.read_text(encoding="utf-8")
+ if version not in text:
+ raise SystemExit(
+ f"{path.relative_to(root)} does not contain release version {version}"
+ )
+
+ print(f"validated {notes_path.relative_to(root)} ({len(notes.split())} words)")
+ PY
diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml
new file mode 100644
index 000000000..6c6299370
--- /dev/null
+++ b/.github/workflows/release-package.yml
@@ -0,0 +1,276 @@
+name: Release package validation
+
+on:
+ pull_request:
+ branches: [master]
+ paths:
+ - "pyproject.toml"
+ - "statgpu/__init__.py"
+ - "setup.py"
+ - "MANIFEST.in"
+ - "README.md"
+ - "CHANGELOG.md"
+ - "docs/en/changelog.md"
+ - "docs/cn/changelog.md"
+ - "RELEASING.md"
+ - ".github/releases/**"
+ - ".github/workflows/publish.yml"
+ - ".github/workflows/release-package.yml"
+ - ".github/workflows/release-notes.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ validate-distributions:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install release tooling
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install build twine
+
+ - name: Verify version declarations
+ run: |
+ python - <<'PY'
+ import pathlib
+ import re
+ import tomllib
+
+ pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
+ init_text = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8")
+ match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_text, re.M)
+ if match is None:
+ raise SystemExit("statgpu/__init__.py does not declare __version__")
+
+ project_version = pyproject["project"]["version"]
+ package_version = match.group(1)
+ if project_version != package_version:
+ raise SystemExit(
+ f"version mismatch: pyproject.toml={project_version}, "
+ f"statgpu/__init__.py={package_version}"
+ )
+ print(project_version)
+ PY
+
+ - name: Build wheel and source distribution
+ env:
+ STATGPU_NO_EXT: "1"
+ run: |
+ rm -rf build dist *.egg-info statgpu.egg-info
+ python -m build
+
+ - name: Check distribution metadata
+ run: python -m twine check dist/*
+
+ - name: Validate artifact names and contents
+ run: |
+ python - <<'PY'
+ import pathlib
+ import re
+ import tarfile
+ import tomllib
+ import zipfile
+
+ root = pathlib.Path.cwd()
+ dist = root / "dist"
+ version = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
+ wheel = dist / f"statgpu-{version}-py3-none-any.whl"
+ sdist = dist / f"statgpu-{version}.tar.gz"
+
+ if not wheel.is_file():
+ raise SystemExit(f"missing expected universal wheel: {wheel.name}")
+ if not sdist.is_file():
+ raise SystemExit(f"missing expected source distribution: {sdist.name}")
+
+ artifacts = sorted(path.name for path in dist.iterdir() if path.is_file())
+ expected = sorted([wheel.name, sdist.name])
+ if artifacts != expected:
+ raise SystemExit(f"unexpected dist contents: {artifacts}; expected {expected}")
+
+ def validate_paths(names, archive):
+ for raw_name in names:
+ path = pathlib.PurePosixPath(raw_name)
+ if path.is_absolute() or ".." in path.parts:
+ raise SystemExit(f"unsafe path in {archive}: {raw_name}")
+ if any(part in {".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} for part in path.parts):
+ raise SystemExit(f"cache or repository metadata in {archive}: {raw_name}")
+ if path.name in {".env", "credentials.json"} or path.suffix in {".pem", ".key"}:
+ raise SystemExit(f"credential-like file in {archive}: {raw_name}")
+
+ with zipfile.ZipFile(wheel) as archive:
+ wheel_names = archive.namelist()
+ validate_paths(wheel_names, wheel.name)
+ if "statgpu/__init__.py" not in wheel_names:
+ raise SystemExit("wheel does not contain statgpu/__init__.py")
+ if any(name.endswith((".so", ".pyd", ".dll", ".dylib")) for name in wheel_names):
+ raise SystemExit("universal wheel unexpectedly contains compiled binaries")
+
+ with tarfile.open(sdist, "r:gz") as archive:
+ sdist_names = set(archive.getnames())
+ validate_paths(sdist_names, sdist.name)
+
+ cython_sources = sorted(
+ path.relative_to(root).as_posix()
+ for path in (root / "statgpu").rglob("*")
+ if path.is_file() and path.suffix in {".pyx", ".pxd"}
+ )
+ if not cython_sources:
+ raise SystemExit("repository contains no optional Cython sources to validate")
+
+ sdist_prefix = f"statgpu-{version}/"
+ missing_cython_sources = [
+ path
+ for path in cython_sources
+ if f"{sdist_prefix}{path}" not in sdist_names
+ ]
+ if missing_cython_sources:
+ raise SystemExit(
+ "sdist is missing repository Cython sources: "
+ + ", ".join(missing_cython_sources)
+ )
+
+ metadata = next(name for name in wheel_names if name.endswith(".dist-info/METADATA"))
+ with zipfile.ZipFile(wheel) as archive:
+ metadata_text = archive.read(metadata).decode("utf-8")
+ if not re.search(rf"^Version: {re.escape(version)}$", metadata_text, re.M):
+ raise SystemExit("wheel metadata version does not match pyproject.toml")
+
+ print(
+ f"validated {wheel.name}, {sdist.name}, and "
+ f"{len(cython_sources)} repository Cython sources"
+ )
+ PY
+
+ - name: Smoke-install sdist in a clean environment
+ env:
+ STATGPU_NO_EXT: "1"
+ run: |
+ SDIST="$(realpath dist/*.tar.gz)"
+ python -m venv "$RUNNER_TEMP/statgpu-sdist-test"
+ "$RUNNER_TEMP/statgpu-sdist-test/bin/python" -m pip install --upgrade pip
+ "$RUNNER_TEMP/statgpu-sdist-test/bin/python" -m pip install "$SDIST"
+ cd "$RUNNER_TEMP"
+ "$RUNNER_TEMP/statgpu-sdist-test/bin/python" - <<'PY'
+ import os
+ import pathlib
+ import tomllib
+ import statgpu
+
+ pyproject = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "pyproject.toml"
+ expected = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"]
+ assert statgpu.__version__ == expected
+ print(statgpu.__version__)
+ PY
+
+ - name: Upload validated distributions
+ uses: actions/upload-artifact@v4
+ with:
+ name: statgpu-release-distributions
+ path: dist/*
+ if-no-files-found: error
+ retention-days: 7
+
+ smoke-install-wheel:
+ name: wheel smoke (${{ matrix.os }})
+ needs: validate-distributions
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Download validated distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: statgpu-release-distributions
+ path: dist
+
+ - name: Smoke-install universal wheel
+ shell: python
+ run: |
+ import os
+ import pathlib
+ import subprocess
+ import tempfile
+ import tomllib
+ import venv
+
+ root = pathlib.Path(os.environ["GITHUB_WORKSPACE"])
+ version = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
+ wheel = root / "dist" / f"statgpu-{version}-py3-none-any.whl"
+ if not wheel.is_file():
+ raise SystemExit(f"missing downloaded wheel: {wheel}")
+
+ temp_root = pathlib.Path(tempfile.mkdtemp(prefix="statgpu-wheel-smoke-"))
+ env_dir = temp_root / "venv"
+ venv.EnvBuilder(with_pip=True, clear=True).create(env_dir)
+ if os.name == "nt":
+ env_python = env_dir / "Scripts" / "python.exe"
+ else:
+ env_python = env_dir / "bin" / "python"
+
+ subprocess.run(
+ [str(env_python), "-m", "pip", "install", "--upgrade", "pip"],
+ check=True,
+ )
+ subprocess.run(
+ [str(env_python), "-m", "pip", "install", str(wheel)],
+ check=True,
+ )
+
+ smoke_code = r'''
+ import os
+ import numpy as np
+ import statgpu
+ from statgpu.linear_model import LinearRegression
+ from statgpu.survival import CoxPH, CoxPHCV
+
+ expected = os.environ["EXPECTED_STATGPU_VERSION"]
+ assert statgpu.__version__ == expected
+
+ X = np.array(
+ [
+ [0.0, 0.0],
+ [1.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 1.0],
+ [2.0, 1.0],
+ [1.0, 2.0],
+ ],
+ dtype=float,
+ )
+ y = 1.0 + 2.0 * X[:, 0] - 0.5 * X[:, 1]
+ model = LinearRegression(device="cpu")
+ model.fit(X, y)
+ prediction = np.asarray(model.predict(X))
+ assert prediction.shape == y.shape
+ assert np.isfinite(prediction).all()
+ assert CoxPH is not None
+ assert CoxPHCV is not None
+ print(statgpu.__version__)
+ '''
+
+ smoke_env = os.environ.copy()
+ smoke_env["EXPECTED_STATGPU_VERSION"] = version
+ subprocess.run(
+ [str(env_python), "-c", smoke_code],
+ cwd=temp_root,
+ env=smoke_env,
+ check=True,
+ )
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c0a45ddb..282c12a5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,30 @@
# Changelog
-All notable changes to statgpu are documented here, organized by date and PR.
+All notable changes to statgpu are documented here, organized by release and date.
-## 2026-08-04
+## 0.2.3 — 2026-08-04
-### PR #80 — Exact-source CV review-fix follow-up
-- Bound the canonical physical-GPU suites to the files actually imported from the audited checkout, including runtime module paths and SHA-256 hashes.
-- Converted requested CoxPHCV two-stage/successive-halving execution into one explicit exhaustive full-precision pass on NumPy, CuPy, and Torch, eliminating the repeated CuPy full-grid fit.
-- Made one-shot `CoxPHCV.cv_splits` iterators reusable across repeated fit, scikit-learn clone, parameter reconstruction, and pickle without rewriting the public constructor attribute during fit.
-- Published the unchanged exact-head `a726937a39eb0ed5a370dd03362884b63a9e9818` physical artifact as a durable Gist: 134/134 checks passed, all return codes were zero, every gate-failure array was empty, and the artifact SHA-256 is `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`.
-- Bumped the final promotion report's machine schema to 3, synchronized primary CoxPH documentation and review status, and returned `.markdown` changelog archives to maintained documentation checks. These follow-up commits create a new head, so final exact-head physical promotion must be rerun before approval.
+### Added
+- Completed CoxPH Phase 1 with Breslow, Efron, and Exact ties; delayed-entry and `(start, stop]` counting-process data; shared-coefficient stratification; subject identifiers; and `Surv(start, stop, event)` formula input.
+- Added shared NumPy, CuPy, and Torch-CUDA risk-set primitives for Cox objectives, gradients, information matrices, and baseline estimation, including backend-native dynamic programming for Exact ties.
+- Extended `CoxPHCV` held-out partial likelihood to all supported tie methods, delayed entry, start-stop rows, strata, and subject-grouped folds.
+
+### Changed
+- Hardened Cox inference, numerical stability, formula NA alignment, singular-information handling, CV cache identity, fold eligibility, selected-penalty refitting, and failed-fit state resets.
+- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation; removed the unidentified intercept; corrected Cox-specific warm starts; and made Torch Efron value, gradient, and Hessian paths native.
+- Standardized public Group Lasso and Adaptive Group Lasso behavior through the generic loss-gradient and exact group-proximal path across supported backends.
+- Made requested CoxPHCV two-stage and successive-halving controls execute one explicit exhaustive full-precision candidate pass, avoiding repeated complete-grid fitting while preserving deterministic selection semantics.
+- Made one-shot `CoxPHCV.cv_splits` iterators reusable across repeated fit, scikit-learn clone, parameter reconstruction, and pickle.
+
+### Validation
+- Hosted workflow #960 passed on the final reviewed head `f05a44ad363b46612e956e137e2f00d040765acb`: documentation, static, full CPU, and Python 3.9–3.12 regression jobs all passed; the complete CPU suite reported 1881 passed and 662 skipped.
+- The final exact-head physical-GPU promotion artifact is published at https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4. It records schema 3, 134/134 passing checks, zero child and nested return codes, empty gate-failure arrays, clean source state before and after execution, and SHA-256 `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`.
+- Added release-package validation that checks version consistency, builds the pure-Python wheel and sdist, runs `twine check`, validates artifact contents, clean-installs the sdist on Ubuntu, and clean-installs the same wheel on Ubuntu, Windows, and macOS.
+
+### Packaging and release publication
+- Bumped the package version to `0.2.3` in `pyproject.toml` and `statgpu/__init__.py`.
+- The official wheel remains a universal `py3-none-any` artifact built with `STATGPU_NO_EXT=1`; optional Cython sources remain available in the sdist.
+- Added the authoritative GitHub Release document at `.github/releases/v0.2.3.md`, a release-note completeness gate, and tag automation that publishes that file as the GitHub Release body after the PyPI job succeeds.
## Earlier history
diff --git a/RELEASING.md b/RELEASING.md
index 773db49d5..dd9397aa1 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,6 +1,13 @@
-# Releasing statgpu to PyPI
+# Releasing statgpu to PyPI and GitHub
-This document is for maintainers preparing an official `statgpu` release. The repository currently publishes from GitHub Actions when a tag matching `v*` is pushed. The workflow is defined in [`.github/workflows/publish.yml`](.github/workflows/publish.yml).
+This document is for maintainers preparing an official `statgpu` release.
+The repository publishes when a tag matching `v*` is pushed.
+
+The release automation is defined in:
+
+- [`.github/workflows/publish.yml`](.github/workflows/publish.yml) for PyPI and GitHub Release publication;
+- [`.github/workflows/release-package.yml`](.github/workflows/release-package.yml) for wheel and sdist validation;
+- [`.github/workflows/release-notes.yml`](.github/workflows/release-notes.yml) for versioned GitHub Release-note validation.
## Release model
@@ -9,163 +16,162 @@ The package version is maintained in two files and must match:
- `pyproject.toml`: `project.version`;
- `statgpu/__init__.py`: `__version__`.
-A release tag must use the same version with a leading `v`, for example:
+A release tag uses the same version with a leading `v`:
```text
-package version: 0.2.2
-tag: v0.2.2
+package version: 0.2.3
+tag: v0.2.3
```
-PyPI release files are immutable. A broken upload cannot be replaced under the same version; prepare a new patch version instead.
-
-## 1. Prepare a focused release pull request
+Each release also has one authoritative GitHub Release body:
-Start from the latest `master` after the intended feature/fix pull requests are merged.
+```text
+.github/releases/vX.Y.Z.md
+```
-Update both version declarations:
+For example, the GitHub Release notes for 0.2.3 are stored at:
-```toml
-# pyproject.toml
-version = "0.2.2"
+```text
+.github/releases/v0.2.3.md
```
-```python
-# statgpu/__init__.py
-__version__ = "0.2.2"
-```
+The tag workflow publishes this file verbatim as the GitHub Release body. Do not
+rely on GitHub's automatically generated PR list as the primary release notes,
+and do not compose the final release body manually in the GitHub UI.
-Update release-facing documentation:
+PyPI release files are immutable. A broken upload cannot be replaced under the
+same version; prepare a new patch version instead.
-- `CHANGELOG.md`;
-- `docs/en/changelog.md`;
-- `docs/cn/changelog.md`;
-- README or model documentation when installation, compatibility, or public behavior changed.
+## 1. Prepare a focused release pull request
-Keep release-only changes separate from large implementation work. The release pull request should primarily contain version, packaging, changelog, and release-validation updates.
+Start from the latest `master` after the intended feature and fix pull requests
+are merged.
-## 2. Validate the release candidate
+Update both version declarations and all release-facing sources:
-At minimum, run the full CPU suite:
+- `pyproject.toml`;
+- `statgpu/__init__.py`;
+- `.github/releases/vX.Y.Z.md`;
+- `CHANGELOG.md`;
+- `docs/en/changelog.md`;
+- `docs/cn/changelog.md`;
+- README or model documentation when installation, compatibility, limitations,
+ or public behavior changed.
+
+The versioned GitHub Release document must be user-facing. It should explain:
+
+- what major capability was added or changed;
+- which public APIs and workflows are affected;
+- installation and platform support;
+- behavioral changes and upgrade implications;
+- known limitations and unsupported combinations;
+- validation evidence without turning the document into an internal audit log;
+- links to the main implementation PR, release PR, version comparison, and
+ repository changelog.
+
+Keep release-only changes separate from implementation work. A release pull
+request should primarily contain version, packaging, changelog, release notes,
+and release-validation changes.
+
+## 2. Validate the release pull request
+
+The normal `Tests` workflow must pass, including the complete CPU suite, static
+contracts, documentation contracts, and the Python 3.9–3.12 regression matrix.
+For changes affecting CuPy, Torch, inference, device routing, or performance,
+record physical-GPU acceptance on the exact release source commit.
+
+### Package validation
+
+The `Release package validation` workflow automatically:
+
+1. checks that `pyproject.toml` and `statgpu/__init__.py` declare the same version;
+2. builds a pure-Python wheel and source distribution with `STATGPU_NO_EXT=1`;
+3. runs `twine check`;
+4. requires exactly `statgpu-X.Y.Z-py3-none-any.whl` and
+ `statgpu-X.Y.Z.tar.gz`;
+5. rejects unsafe paths, credential-like files, cache directories, and compiled
+ binaries in the universal wheel;
+6. confirms that the sdist contains every `.pyx` or `.pxd` source that currently
+ exists in the repository;
+7. installs the sdist in a clean Ubuntu virtual environment and checks its
+ version;
+8. uploads the validated wheel and sdist as a short-lived workflow artifact;
+9. downloads that exact wheel artifact on Ubuntu, Windows, and macOS, installs it
+ in a fresh virtual environment, imports the public linear-model and Cox APIs,
+ and runs a CPU `LinearRegression` fit/predict smoke test.
+
+The cross-platform matrix validates portability of the published
+`py3-none-any` CPU wheel. It does not claim Apple MPS support or replace the
+separate physical-NVIDIA-GPU acceptance required for CUDA behavior.
+
+### GitHub Release-note validation
+
+The `Release notes validation` workflow requires:
+
+- `.github/releases/vX.Y.Z.md` matching the package version;
+- a title of the form `# statgpu X.Y.Z`;
+- substantive Highlights, Installation and platform support, Validation,
+ Upgrade notes and known limits, and Full change history sections;
+- no unresolved `TODO`, `TBD`, `X.Y.Z`, or similar placeholders;
+- explicit installation, platform, and version-comparison information;
+- the same version to appear in the root, English, and Chinese changelogs.
+
+This gate prevents a release tag from being prepared with a generic or
+incomplete GitHub Release description.
+
+For a local rehearsal, run:
```bash
python -m pip install -e ".[dev,validation,formula]"
python -m pytest dev/tests -q --tb=short
-```
-Run focused physical-GPU acceptance for changes that affect CuPy, Torch, inference, device routing, or performance. Record the exact commit, GPU, CUDA/CuPy/Torch versions, and whether any test was skipped.
-
-Confirm that both version declarations agree:
-
-```bash
-python - <<'PY'
-import pathlib
-import re
-
-pyproject = pathlib.Path("pyproject.toml").read_text(encoding="utf-8")
-init_file = pathlib.Path("statgpu/__init__.py").read_text(encoding="utf-8")
-
-project_version = re.search(r'^version\s*=\s*["\']([^"\']+)["\']', pyproject, re.M).group(1)
-package_version = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', init_file, re.M).group(1)
-assert project_version == package_version, (project_version, package_version)
-print(project_version)
-PY
-```
-
-## 3. Build clean artifacts locally
-
-Remove stale packaging output first:
-
-```bash
rm -rf build dist *.egg-info statgpu.egg-info
python -m pip install --upgrade build twine
-```
-
-The official PyPI workflow sets `STATGPU_NO_EXT=1`. This produces a universal pure-Python wheel while retaining optional Cython sources in the sdist:
-
-```bash
STATGPU_NO_EXT=1 python -m build
python -m twine check dist/*
ls -lh dist/
```
-Expected artifacts:
-
-```text
-statgpu-X.Y.Z-py3-none-any.whl
-statgpu-X.Y.Z.tar.gz
-```
-
-`MANIFEST.in` includes the `.pyx` and `.pxd` files required by users who choose to build the optional CPU extensions from the sdist.
-
-## 4. Test the wheel and sdist in clean environments
-
-Do not validate only from the source checkout. Install each artifact in a fresh environment.
-
-### Wheel
-
-```bash
-python -m venv /tmp/statgpu-wheel-test
-/tmp/statgpu-wheel-test/bin/python -m pip install --upgrade pip
-/tmp/statgpu-wheel-test/bin/python -m pip install dist/statgpu-X.Y.Z-py3-none-any.whl
-/tmp/statgpu-wheel-test/bin/python - <<'PY'
-import statgpu
-print(statgpu.__version__)
-from statgpu.linear_model import LinearRegression
-print(LinearRegression)
-PY
-```
-
-### Source distribution
-
-```bash
-python -m venv /tmp/statgpu-sdist-test
-/tmp/statgpu-sdist-test/bin/python -m pip install --upgrade pip
-STATGPU_NO_EXT=1 /tmp/statgpu-sdist-test/bin/python -m pip install dist/statgpu-X.Y.Z.tar.gz
-/tmp/statgpu-sdist-test/bin/python - <<'PY'
-import statgpu
-print(statgpu.__version__)
-PY
-```
-
-On Windows, replace `/tmp/.../bin/python` with the environment's `Scripts/python.exe`.
+Do not validate only from the source checkout. Install the wheel and sdist in
+fresh environments, or rely on the successful release-package workflow for the
+exact PR head.
-For packaging changes, also inspect the artifact contents and confirm that no credentials, benchmark caches, local configuration, or unrelated result bundles are included.
+## 3. Optional TestPyPI rehearsal
-## 5. Optional TestPyPI rehearsal
-
-A TestPyPI upload is recommended when changing packaging metadata, package discovery, build behavior, dependencies, or release automation.
+A TestPyPI rehearsal is recommended when changing packaging metadata, package
+discovery, build behavior, dependencies, or release automation:
```bash
python -m twine upload --repository testpypi dist/*
-```
-
-Install with PyPI available for dependencies:
-
-```bash
python -m pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
statgpu==X.Y.Z
```
-TestPyPI and PyPI require separate credentials/tokens.
+TestPyPI and PyPI use separate credentials.
-## 6. Merge the release pull request
+## 4. Merge the release pull request
Before merging, verify:
-- version fields match;
-- changelogs describe the release accurately;
-- CI is green on the exact release head;
-- required physical-GPU tests are recorded;
-- wheel and sdist both pass `twine check` and clean-install tests;
-- the target version does not already exist on PyPI.
+- both version declarations match the intended release;
+- `.github/releases/vX.Y.Z.md` accurately describes the user-visible release;
+- root, English, and Chinese changelogs are synchronized;
+- required GitHub Actions jobs are green on the exact release head;
+- required physical-GPU evidence is recorded for the exact source commit;
+- wheel and sdist validation, the Ubuntu sdist clean-install check, and the
+ Ubuntu/Windows/macOS wheel smoke matrix pass;
+- the target version does not already exist on PyPI;
+- the `PYPI_TOKEN` repository secret remains valid and project-scoped.
-Merge the focused release pull request into `master`.
+Merge the focused release pull request into `master`. Do not add unrelated
+commits after release validation; changes after validation require the release
+checks to run again.
-## 7. Create and push the release tag
+## 5. Create and push the release tag
-Update local `master` and tag the exact merge commit:
+Update local `master` and tag the exact release-PR merge commit:
```bash
git checkout master
@@ -174,21 +180,32 @@ git tag -a vX.Y.Z -m "statgpu X.Y.Z"
git push origin vX.Y.Z
```
-Pushing the tag starts the `Publish to PyPI` workflow. The current workflow:
+Pushing the tag starts `Publish to PyPI`. The workflow:
1. checks out the tagged commit;
-2. sets up Python 3.11;
-3. installs `build` and `twine`;
-4. verifies that the tag matches `pyproject.toml`;
-5. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`;
-6. runs `twine check`;
-7. uploads `dist/*` to PyPI using the repository secret `PYPI_TOKEN`.
-
-The PyPI API token should be project-scoped and stored only as a GitHub Actions secret. Never place it in source files, command history committed to the repository, issue comments, or documentation examples.
-
-## 8. Verify the published release
-
-After the workflow succeeds, verify the PyPI release in a new environment:
+2. verifies that the tag, `pyproject.toml`, and `statgpu.__version__` agree;
+3. verifies that `.github/releases/vX.Y.Z.md` exists and has the required
+ versioned sections;
+4. builds a pure-Python wheel and sdist with `STATGPU_NO_EXT=1`;
+5. runs `twine check`;
+6. installs the wheel in a clean environment and checks its version and core
+ imports;
+7. retains the validated distributions as a workflow artifact;
+8. uploads the distributions to PyPI using the repository secret `PYPI_TOKEN`;
+9. only after the PyPI job succeeds, creates or updates the GitHub Release using
+ `.github/releases/vX.Y.Z.md` as the exact release body and attaches the same
+ wheel and sdist.
+
+PyPI publication and GitHub Release creation are separate jobs. If the GitHub
+Release job fails after PyPI succeeds, rerun only the failed job; the successful
+PyPI upload does not need to be repeated.
+
+Never place the PyPI token in source files, committed command output, issues,
+pull requests, or documentation examples.
+
+## 6. Verify the published release
+
+After the workflow succeeds, verify the PyPI package from a new environment:
```bash
python -m venv /tmp/statgpu-pypi-test
@@ -196,37 +213,59 @@ python -m venv /tmp/statgpu-pypi-test
/tmp/statgpu-pypi-test/bin/python -m pip install --no-cache-dir statgpu==X.Y.Z
/tmp/statgpu-pypi-test/bin/python - <<'PY'
import statgpu
+from statgpu.survival import CoxPH, CoxPHCV
+
print(statgpu.__version__)
+print(CoxPH, CoxPHCV)
PY
```
Also verify:
-- the PyPI project page renders the README correctly;
-- the wheel is `py3-none-any` as intended;
+- the PyPI page renders the README correctly;
+- the wheel is `py3-none-any`;
- the sdist is present;
-- dependency extras are displayed;
-- the homepage and repository links are valid.
+- dependency extras and supported Python versions are correct;
+- project, documentation, issue, and changelog links work;
+- the GitHub Release title is `statgpu X.Y.Z`;
+- the GitHub Release body matches `.github/releases/vX.Y.Z.md` rather than an
+ automatically generated PR summary;
+- the GitHub Release includes the same wheel and sdist published by the tag
+ workflow.
+
+## 7. Failure handling
-Create a GitHub Release from the same tag and use the changelog as the basis for release notes.
+### Version or release-note mismatch
-## 9. Failure handling
+The publish workflow stops before upload. Correct the version or release-note
+file in a new commit, merge a new release PR, and create a new tag. Do not move a
+published tag.
-### Version mismatch
+### Partial PyPI upload
-If the tag and package version differ, the workflow stops before uploading. Correct the version in a new commit and create a new tag. Do not move an already published tag.
+PyPI may accept one artifact before another fails. Because filenames and
+versions are immutable, inspect the release and normally issue a new patch
+version rather than trying to replace the accepted file.
-### Upload partially succeeds
+### GitHub Release publication failure
-PyPI may accept one artifact before another fails. Because filenames and versions are immutable, inspect the project release and normally issue a new patch version rather than attempting to replace uploaded files.
+If the PyPI job succeeded and only the GitHub Release job failed, rerun the
+failed GitHub Release job. It is idempotent: an existing release is updated from
+the versioned notes file and attached artifacts are uploaded with replacement.
### Bad release already published
-- mark the PyPI release as yanked when appropriate;
+- yank the PyPI release when appropriate;
- fix the problem in a new patch release;
-- document the incident and migration path in the changelog;
-- do not delete or recreate Git history to reuse the version.
-
-## Recommended automation improvement
-
-The current workflow uses a project-scoped API token through `PYPI_TOKEN`. PyPI Trusted Publishing is preferable for long-term maintenance because it removes the stored upload token and binds publishing to a specific GitHub repository/workflow/environment. Migrating should be handled in a dedicated release-infrastructure pull request and tested before removing the existing token path.
+- document the incident and migration path in the changelog and versioned release
+ notes;
+- do not rewrite Git history or reuse the released version.
+
+## Future infrastructure improvement
+
+The current workflow uses a project-scoped API token through `PYPI_TOKEN`.
+PyPI Trusted Publishing is preferable for long-term maintenance because it
+removes the stored upload token and binds publishing to a specific repository,
+workflow, and optional environment. Migrate in a dedicated infrastructure pull
+request and verify the trusted-publisher configuration before removing the token
+path.
diff --git a/docs/cn/changelog.md b/docs/cn/changelog.md
index 1396b902b..ede007d6b 100644
--- a/docs/cn/changelog.md
+++ b/docs/cn/changelog.md
@@ -5,31 +5,48 @@
> 页面定位:变更记录
> 切换:[English](../en/changelog.md)
-## 2026-08
-
-### 修复(2026-08-04)— PR #80 精确源码 CV 复审后续
-
-- 规范物理 GPU suite 现在会把受审计的 Git checkout 放在 `PYTHONPATH`
- 首位、禁用 user site,核验实际导入模块的路径均位于该 checkout 内,并记录这些
- 实际导入文件的 SHA-256;child 与 nested runner 继承同一受控环境。
-- 请求 CoxPHCV two-stage 或 successive-halving 后,NumPy、CuPy 与 Torch 现在都只
- 执行一次显式 exhaustive full-precision candidate pass。公开诊断记录
- `staged_safety_strategy="single_pass_exhaustive"`;不筛除任何 candidate,CuPy 也不再
- 重复完整 grid。
-- 一次性 `CoxPHCV.cv_splits` iterator 会私下 materialize 一次,并在重复 fit、
- scikit-learn clone、旧版参数重建与 pickle 中复用;fit 期间公开构造参数对象保持不变。
-- Hosted workflow #946 已在精确 head
- `a726937a39eb0ed5a370dd03362884b63a9e9818` 上通过:完整 CPU suite 为
- 1879 passed、662 skipped,static、文档及 Python 3.9–3.12 regression job 全部通过。
-- 该 head 的原始物理结果现已持久发布为
- [最终 promotion artifact](https://gist.github.com/TheHiddenObserver/ebbb7f2401f45b124069a30d3510c139)。
- Artifact 记录 134/134 项检查通过、所有 return code 为 0、所有 gate-failure 数组为空,
- SHA-256 为
- `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`。
-- 本后续提交将 final aggregation format 正式升级为 machine schema 3,同步 CoxPH
- 主模型页,并把 `.markdown` 历史页重新纳入维护文档检查。由于这些提交产生了新的
- head,最终批准前必须对新 head 再运行一次 exact-head physical suite;上述 Gist
- 仍只证明 `a726937...`。
+## 0.2.3 — 2026-08-04
+
+### 生存分析
+
+- 完成 CoxPH Phase 1:支持 Breslow、Efron 与 Exact ties,delayed entry、
+ `(start, stop]` counting-process 数据、共享系数的分层模型、subject identifier,
+ 以及 `Surv(start, stop, event)` 公式输入。
+- 为 NumPy、CuPy 与 Torch-CUDA 增加共享的 Cox risk-set objective、gradient、
+ information matrix 与 baseline estimation primitive;Exact tied-event partition
+ 使用 backend-native dynamic programming。
+- `CoxPHCV` 的 held-out partial likelihood 现支持全部 tie method、delayed entry、
+ start-stop row、strata 与按 subject 分组的 fold。
+- 强化 Cox inference、centered risk-set 数值计算、log-domain baseline prediction、
+ 公式 NA 对齐、奇异 information 检查、CV cache identity、fold eligibility、
+ selected-penalty 全数据 refit 与失败 fit 的状态清理。
+- 强化 L1、L2、Elastic Net、SCAD 与 MCP penalized Cox estimation;移除不可识别
+ intercept,修正 Cox-specific warm start,并使 Torch Efron 的 value、gradient
+ 与 Hessian 路径保持原生实现。
+
+### 交叉验证与分组惩罚
+
+- 请求 CoxPHCV two-stage 或 successive-halving 时,统一执行一次显式 exhaustive
+ full-precision candidate pass,在保持确定性选择语义的同时避免重复完整 grid fit。
+- 一次性 `CoxPHCV.cv_splits` iterator 可在重复 fit、scikit-learn clone、参数重建
+ 与 pickle 中复用。
+- 公开 Group Lasso 与 Adaptive Group Lasso 在支持的 backend 上统一采用 generic
+ loss-gradient 与 exact group-proximal 路径。
+
+### 验证与打包
+
+- Hosted workflow #960 已在最终审查 head
+ `f05a44ad363b46612e956e137e2f00d040765acb` 上通过:文档、static、完整 CPU
+ 与 Python 3.9–3.12 regression job 均通过;完整 CPU suite 为 1881 passed、
+ 662 skipped。
+- 最终 exact-head 物理 GPU promotion artifact 已作为
+ [schema-3 evidence](https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4)
+ 持久发布。它记录 134/134 项检查通过、child 与 nested return code 均为 0、
+ gate-failure 数组为空、运行前后源码状态干净,SHA-256 为
+ `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`。
+- 包版本更新为 `0.2.3`。新增 release-package validation:检查版本一致性,构建
+ pure-Python wheel 与 sdist,执行 `twine check`,核验 artifact 内容,并在干净
+ 环境中分别 smoke-install 两种发行包。
## 更早的历史记录
diff --git a/docs/en/changelog.md b/docs/en/changelog.md
index cc5424eb3..8a3288191 100644
--- a/docs/en/changelog.md
+++ b/docs/en/changelog.md
@@ -5,35 +5,51 @@
> This page: Changelog
> Switch: [Chinese](../cn/changelog.md)
-## 2026-08
-
-### Fixed (2026-08-04) — PR #80 exact-source CV review follow-up
-
-- Canonical physical-GPU suites now prepend the audited Git checkout to
- `PYTHONPATH`, disable the user site, verify that actual imported module paths
- remain inside that checkout, and record SHA-256 hashes for those imported
- files. Child and nested runners inherit the same controlled environment.
-- Requested CoxPHCV two-stage and successive-halving controls now produce one
- explicit exhaustive full-precision candidate pass on NumPy, CuPy, and Torch.
- Public diagnostics report `staged_safety_strategy="single_pass_exhaustive"`;
- no candidate is screened out and CuPy no longer repeats the complete grid.
-- One-shot `CoxPHCV.cv_splits` iterators are materialized privately once and
- reused for repeated fits, scikit-learn clone, legacy parameter reconstruction,
- and pickle. Fit retains the original public constructor object.
-- Hosted workflow #946 passed on exact head
- `a726937a39eb0ed5a370dd03362884b63a9e9818`: the full CPU suite reported
- 1879 passed and 662 skipped, while static, documentation, and Python 3.9–3.12
- regression jobs all passed.
-- The unchanged physical result for that head is now durably published as
- [the final promotion artifact](https://gist.github.com/TheHiddenObserver/ebbb7f2401f45b124069a30d3510c139).
- It records 134/134 passing checks, zero return codes, empty gate-failure arrays,
- and SHA-256
- `e01ad0bfec238d06167caeef9955e92b6cf84eea4ccc69a3056eb794ded6eccb`.
-- This follow-up makes the final aggregation format truly machine schema 3,
- synchronizes the primary CoxPH model pages, and brings `.markdown` archives
- back under maintained documentation checks. Because these commits create a
- new head, the final exact-head physical suite must be rerun before approval;
- the published Gist remains valid evidence for `a726937...` only.
+## 0.2.3 — 2026-08-04
+
+### Survival analysis
+
+- Completed CoxPH Phase 1 with Breslow, Efron, and Exact ties; delayed-entry
+ and `(start, stop]` counting-process data; shared-coefficient stratification;
+ subject identifiers; and `Surv(start, stop, event)` formula input.
+- Added shared NumPy, CuPy, and Torch-CUDA risk-set primitives for objectives,
+ gradients, information matrices, and baseline estimation. Exact tied-event
+ partitions use backend-native dynamic programming.
+- Extended `CoxPHCV` held-out partial likelihood to all supported tie methods,
+ delayed entry, start-stop rows, strata, and subject-grouped folds.
+- Hardened Cox inference, centered risk-set numerics, log-domain baseline
+ prediction, formula NA alignment, singular-information handling, CV cache
+ identity, fold eligibility, selected-penalty refitting, and failed-fit state
+ resets.
+- Hardened L1, L2, Elastic Net, SCAD, and MCP penalized Cox estimation; removed
+ the unidentified intercept; corrected Cox-specific warm starts; and made the
+ Torch Efron value, gradient, and Hessian paths native.
+
+### Cross-validation and grouped penalties
+
+- Requested CoxPHCV two-stage and successive-halving controls now execute one
+ explicit exhaustive full-precision candidate pass, preserving deterministic
+ selection while avoiding repeated complete-grid fitting.
+- One-shot `CoxPHCV.cv_splits` iterators are reusable across repeated fit,
+ scikit-learn clone, parameter reconstruction, and pickle.
+- Public Group Lasso and Adaptive Group Lasso use the generic loss-gradient and
+ exact group-proximal path consistently across supported backends.
+
+### Validation and packaging
+
+- Hosted workflow #960 passed on final reviewed head
+ `f05a44ad363b46612e956e137e2f00d040765acb`: documentation, static, full CPU,
+ and Python 3.9–3.12 regression jobs all passed; the complete CPU suite reported
+ 1881 passed and 662 skipped.
+- The final exact-head physical-GPU promotion artifact is published as
+ [schema-3 evidence](https://gist.github.com/TheHiddenObserver/afdcad86a243e68a918d852b92e984a4).
+ It records 134/134 passing checks, zero child and nested return codes, empty
+ gate-failure arrays, clean source state before and after execution, and SHA-256
+ `bd4058450def691dd29e9d78853534016c6da70c33192a97dc312d95cbe5d76d`.
+- The package version is now `0.2.3`. Release-package validation checks version
+ consistency, builds the pure-Python wheel and sdist, runs `twine check`,
+ validates artifact contents, and smoke-installs both distributions in clean
+ environments.
## Earlier history
diff --git a/pyproject.toml b/pyproject.toml
index c7cd9b666..cf24a9812 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "statgpu"
-version = "0.2.2"
+version = "0.2.3"
description = "GPU-accelerated statistical methods with sklearn-compatible API"
readme = "README.md"
requires-python = ">=3.9"
diff --git a/statgpu/__init__.py b/statgpu/__init__.py
index 502362ec9..584b81dbe 100644
--- a/statgpu/__init__.py
+++ b/statgpu/__init__.py
@@ -4,7 +4,7 @@
A sklearn-compatible library for statistical computing with GPU support.
"""
-__version__ = "0.2.2"
+__version__ = "0.2.3"
from ._config import get_device, set_device, Device
from ._base import BaseEstimator