diff --git a/docs/WWPGD_COMMIT_PIN.md b/docs/WWPGD_COMMIT_PIN.md new file mode 100644 index 0000000..ad38b25 --- /dev/null +++ b/docs/WWPGD_COMMIT_PIN.md @@ -0,0 +1,24 @@ +# Optional `WW_PGD` commit pin + +**Default:** floating install from `pyproject.toml` (`ww-pgd @ git+…`), unchanged. + +**Optional pin** for joint runs / freeze protocols: + +```bash +export WWPGD_COMMIT_PIN= +``` + +When set, training with a WW-PGD extension **fails at run setup** if the installed package’s PEP 610 VCS `commit_id` does not match (prefix match allowed). + +| Env | Effect | +|---|---| +| unset / empty | floating install (default) | +| set to SHA | require installed `ww_pgd` commit match | + +Provenance fields in the run manifest: + +- `wwpgd_resolved_commit` +- `wwpgd_commit_pin_requested` +- `wwpgd_dependency_pinned` (true only when env is set) + +This does **not** change spectral targets, dose, or training defaults. It only makes actuator version drift fail loudly when you ask for a pin. diff --git a/src/wwgpt/pip_wwpgd_adapter.py b/src/wwgpt/pip_wwpgd_adapter.py index 69a475f..407f540 100644 --- a/src/wwgpt/pip_wwpgd_adapter.py +++ b/src/wwgpt/pip_wwpgd_adapter.py @@ -3,10 +3,15 @@ import inspect import json +import os from importlib import metadata from pathlib import Path from typing import Any +# Optional pin for joint/repro runs. Empty = floating install (current default). +# Accepts full or prefix SHA from pip VCS install (direct_url.json commit_id). +WWPGD_COMMIT_PIN_ENV = "WWPGD_COMMIT_PIN" + REQUIRED_PROJECTOR_PARAMETERS = frozenset( {"model", "cfg", "epoch", "num_epochs", "global_step", "ww_logs", "layer_selector"} ) @@ -76,22 +81,53 @@ def resolve_pip_wwpgd_provenance() -> dict[str, Any]: import weightwatcher weightwatcher_dist = _distribution("weightwatcher") + requested_pin = (os.environ.get(WWPGD_COMMIT_PIN_ENV) or "").strip() + resolved_commit = vcs.get("commit_id") + pinned = bool(requested_pin) return { "wwpgd_distribution_name": str(ww_dist_name), "wwpgd_installed_version": str(ww_version), "wwpgd_module_path": str(Path(ww_pgd.__file__).resolve()), "wwpgd_source_url": direct.get("url"), "wwpgd_install_mode": mode, - "wwpgd_resolved_commit": vcs.get("commit_id"), + "wwpgd_resolved_commit": resolved_commit, + "wwpgd_commit_pin_env": WWPGD_COMMIT_PIN_ENV, + "wwpgd_commit_pin_requested": requested_pin or None, "wwpgd_projector_signature": str(api["projector_signature_object"]), "wwpgd_config_signature": str(api["config_signature_object"]), "wwpgd_native_internal_diagnostics": api["native_internal_diagnostics"], - "wwpgd_dependency_pinned": False, + # True only when the caller requested a pin via env (default remains floating). + "wwpgd_dependency_pinned": pinned, "weightwatcher_installed_version": str(weightwatcher_dist.version if weightwatcher_dist else getattr(weightwatcher, "__version__", "unknown")), "weightwatcher_module_path": str(Path(weightwatcher.__file__).resolve()), } +def assert_wwpgd_commit_pin(provenance: dict[str, Any] | None = None) -> dict[str, Any]: + """If ``WWPGD_COMMIT_PIN`` is set, require the installed VCS commit to match. + + Default behavior (env unset/empty): no-op, floating ``ww_pgd`` install allowed. + Match is prefix-based so a short SHA pin is valid against a full commit id. + """ + info = provenance if provenance is not None else resolve_pip_wwpgd_provenance() + pin = str(info.get("wwpgd_commit_pin_requested") or "").strip() + if not pin: + return info + resolved = str(info.get("wwpgd_resolved_commit") or "").strip() + if not resolved: + raise RuntimeError( + f"{WWPGD_COMMIT_PIN_ENV}={pin!r} is set but the installed ww_pgd package " + "has no VCS commit in PEP 610 direct_url.json (e.g. pure PyPI install). " + "Install from git or clear the pin." + ) + if not (resolved.startswith(pin) or pin.startswith(resolved)): + raise RuntimeError( + f"ww_pgd commit pin mismatch: {WWPGD_COMMIT_PIN_ENV}={pin!r} " + f"but resolved commit is {resolved!r}" + ) + return info + + def construct_pip_wwpgd_config(spec: object) -> tuple[object, dict[str, Any]]: """Map every mathematical experiment option into the installed config.""" api = inspect_pip_wwpgd_api() diff --git a/src/wwgpt/train.py b/src/wwgpt/train.py index d7c3c1b..aec1fc0 100644 --- a/src/wwgpt/train.py +++ b/src/wwgpt/train.py @@ -1714,6 +1714,11 @@ def run_scientific_single( "optimizer_implementation_version": bundle.implementation_versions, }) man.update(code_version) + if extension_name in INTERVENTION_EXTENSIONS: + # Optional env WWPGD_COMMIT_PIN: fail fast if installed ww_pgd commit mismatches. + # Default (unset) keeps floating git install behavior unchanged. + from wwgpt.pip_wwpgd_adapter import assert_wwpgd_commit_pin + assert_wwpgd_commit_pin() man.update(external_wwpgd_manifest_fields(extension_name in INTERVENTION_EXTENSIONS, cfg.wwpgd if extension_name in INTERVENTION_EXTENSIONS else None)) if cached_mode: adaptive = cfg.wwpgd.adaptive diff --git a/tests/test_wwpgd_commit_pin.py b/tests/test_wwpgd_commit_pin.py new file mode 100644 index 0000000..33e3c46 --- /dev/null +++ b/tests/test_wwpgd_commit_pin.py @@ -0,0 +1,34 @@ +"""Optional WWPGD_COMMIT_PIN env — Class C logging/repro only; default floating.""" +from __future__ import annotations + +import pytest + +from wwgpt.pip_wwpgd_adapter import ( + WWPGD_COMMIT_PIN_ENV, + assert_wwpgd_commit_pin, +) + + +def test_commit_pin_noop_when_env_unset(monkeypatch): + monkeypatch.delenv(WWPGD_COMMIT_PIN_ENV, raising=False) + info = assert_wwpgd_commit_pin() + assert info.get("wwpgd_commit_pin_requested") in (None, "") + assert info.get("wwpgd_dependency_pinned") is False + + +def test_commit_pin_accepts_matching_prefix(monkeypatch): + monkeypatch.delenv(WWPGD_COMMIT_PIN_ENV, raising=False) + base = assert_wwpgd_commit_pin() + resolved = str(base.get("wwpgd_resolved_commit") or "") + if not resolved: + pytest.skip("installed ww_pgd has no VCS commit to pin against") + monkeypatch.setenv(WWPGD_COMMIT_PIN_ENV, resolved[:12]) + info = assert_wwpgd_commit_pin() + assert info["wwpgd_dependency_pinned"] is True + assert str(info["wwpgd_commit_pin_requested"]).startswith(resolved[:12]) + + +def test_commit_pin_rejects_mismatch(monkeypatch): + monkeypatch.setenv(WWPGD_COMMIT_PIN_ENV, "0" * 40) + with pytest.raises(RuntimeError, match="commit pin mismatch|no VCS commit"): + assert_wwpgd_commit_pin()