invarlock 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {invarlock-0.3.0/src/invarlock.egg-info → invarlock-0.3.1}/PKG-INFO +2 -2
- {invarlock-0.3.0 → invarlock-0.3.1}/README.md +1 -1
- {invarlock-0.3.0 → invarlock-0.3.1}/pyproject.toml +1 -1
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/__init__.py +1 -1
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/_data/runtime/profiles/ci_cpu.yaml +5 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/doctor.py +7 -1
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/run.py +148 -2
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/certificate.py +155 -15
- {invarlock-0.3.0 → invarlock-0.3.1/src/invarlock.egg-info}/PKG-INFO +2 -2
- {invarlock-0.3.0 → invarlock-0.3.1}/LICENSE +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/MANIFEST.in +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/setup.cfg +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/__main__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/_data/runtime/profiles/release.yaml +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/_data/runtime/tiers.yaml +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/_capabilities.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/auto.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/base.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/base_types.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/capabilities.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_bert.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_gpt2.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_llama.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_mixin.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_onnx.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/hf_t5.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/adapters/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/assurance/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/__main__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/_evidence.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/_json.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/adapter_auto.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/app.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/certify.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/explain_gates.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/export_html.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/plugins.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/report.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/commands/verify.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/config.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/constants.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/device.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/doctor_helpers.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/errors.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/overhead_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/provenance.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/cli/utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/config.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/abi.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/api.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/auto_tuning.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/bootstrap.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/checkpoint.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/contracts.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/error_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/events.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/exceptions.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/registry.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/retry.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/runner.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/core/types.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/_edit_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/_external_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/noop.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/quant_rtn.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/edits/registry.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/bench.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/bootstrap.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/data.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/metrics.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/primary_metric.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/probes/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/probes/fft.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/probes/mi.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/probes/post_attention.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/providers/base.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/providers/seq2seq.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/providers/text_lm.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/providers/vision_text.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/eval/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/_contracts.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/invariants.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/policies.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/rmt.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/spectral.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/tier_config.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards/variance.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards_ref/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards_ref/rmt_ref.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards_ref/spectral_ref.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/guards_ref/variance_ref.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/model_profile.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/model_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/alerting.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/core.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/exporters.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/health.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/metrics.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/observability/utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/hello_guard.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/hf_awq_adapter.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/hf_bnb_adapter.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/hf_gptq_adapter.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/plugins/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/py.typed +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/certificate_schema.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/dataset_hashing.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/guards_analysis.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/html.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/normalizer.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/policy_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/primary_metric_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/render.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/report.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/report_types.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/reporting/validate.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/security.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/sparsity_utils.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/utils/__init__.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock/utils/digest.py +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock.egg-info/SOURCES.txt +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock.egg-info/dependency_links.txt +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock.egg-info/entry_points.txt +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock.egg-info/requires.txt +0 -0
- {invarlock-0.3.0 → invarlock-0.3.1}/src/invarlock.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invarlock
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Edit‑agnostic robustness certificates for weight edits (InvarLock framework)
|
|
5
5
|
Author-email: InvarLock Team <oss@invarlock.dev>
|
|
6
6
|
Maintainer-email: InvarLock Maintainers <support@invarlock.dev>
|
|
@@ -112,7 +112,7 @@ they don’t, roll back safely.
|
|
|
112
112
|
Technical: edit‑agnostic guard pipeline (invariants → spectral → RMT →
|
|
113
113
|
variance) producing a machine‑readable Safety Certificate.
|
|
114
114
|
|
|
115
|
-
> **Status:** 0.3.
|
|
115
|
+
> **Status:** 0.3.1 (pre‑1.0). Until 1.0, **minor** releases may be
|
|
116
116
|
> breaking. See CLI help and the CHANGELOG for updates.
|
|
117
117
|
|
|
118
118
|
[](https://github.com/invarlock/invarlock/actions/workflows/ci.yml)
|
|
@@ -6,7 +6,7 @@ they don’t, roll back safely.
|
|
|
6
6
|
Technical: edit‑agnostic guard pipeline (invariants → spectral → RMT →
|
|
7
7
|
variance) producing a machine‑readable Safety Certificate.
|
|
8
8
|
|
|
9
|
-
> **Status:** 0.3.
|
|
9
|
+
> **Status:** 0.3.1 (pre‑1.0). Until 1.0, **minor** releases may be
|
|
10
10
|
> breaking. See CLI help and the CHANGELOG for updates.
|
|
11
11
|
|
|
12
12
|
[](https://github.com/invarlock/invarlock/actions/workflows/ci.yml)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "invarlock"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Edit‑agnostic robustness certificates for weight edits (InvarLock framework)"
|
|
9
9
|
authors = [{ name = "InvarLock Team", email = "oss@invarlock.dev" }]
|
|
10
10
|
maintainers = [{ name = "InvarLock Maintainers", email = "support@invarlock.dev" }]
|
|
@@ -12,7 +12,7 @@ For torch-dependent functionality, see subpackages under `invarlock.*`:
|
|
|
12
12
|
- `invarlock.eval`: Metrics, guard-overhead checks, and certification
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
__version__ = "0.3.
|
|
15
|
+
__version__ = "0.3.1"
|
|
16
16
|
|
|
17
17
|
# Core exports - torch-independent
|
|
18
18
|
from .config import CFG, Defaults, get_default_config
|
|
@@ -326,8 +326,14 @@ def doctor_command(
|
|
|
326
326
|
try:
|
|
327
327
|
import torch
|
|
328
328
|
|
|
329
|
+
torch_version = getattr(torch, "__version__", None)
|
|
329
330
|
if not json_out:
|
|
330
|
-
|
|
331
|
+
if torch_version:
|
|
332
|
+
console.print(f"[green]✅ PyTorch {torch_version}[/green]")
|
|
333
|
+
else:
|
|
334
|
+
console.print(
|
|
335
|
+
"[yellow]⚠️ PyTorch present but version unavailable[/yellow]"
|
|
336
|
+
)
|
|
331
337
|
|
|
332
338
|
# Device information
|
|
333
339
|
from ..device import get_device_info
|
|
@@ -81,6 +81,137 @@ GUARD_OVERHEAD_THRESHOLD = 0.01
|
|
|
81
81
|
SPLIT_ALIASES: tuple[str, ...] = ("validation", "val", "dev", "eval", "test")
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def _coerce_mapping(obj: object) -> dict[str, Any]:
|
|
85
|
+
"""Best-effort conversion of config-like objects to plain dicts."""
|
|
86
|
+
|
|
87
|
+
if isinstance(obj, dict):
|
|
88
|
+
return obj
|
|
89
|
+
try:
|
|
90
|
+
raw = getattr(obj, "_data", None)
|
|
91
|
+
if isinstance(raw, dict):
|
|
92
|
+
return raw
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
try:
|
|
96
|
+
dumped = obj.model_dump() # type: ignore[attr-defined]
|
|
97
|
+
if isinstance(dumped, dict):
|
|
98
|
+
return dumped
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
try:
|
|
102
|
+
data = vars(obj)
|
|
103
|
+
if isinstance(data, dict):
|
|
104
|
+
return data
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_pm_acceptance_range(
|
|
111
|
+
cfg: InvarLockConfig | dict[str, Any] | None,
|
|
112
|
+
) -> dict[str, float]:
|
|
113
|
+
"""Resolve primary-metric acceptance bounds from config/env with safe defaults."""
|
|
114
|
+
|
|
115
|
+
base_min = 0.95
|
|
116
|
+
base_max = 1.10
|
|
117
|
+
|
|
118
|
+
cfg_min = None
|
|
119
|
+
cfg_max = None
|
|
120
|
+
try:
|
|
121
|
+
cfg_map = _coerce_mapping(cfg) if cfg is not None else {}
|
|
122
|
+
pm_section = cfg_map.get("primary_metric") if isinstance(cfg_map, dict) else {}
|
|
123
|
+
pm_map = _coerce_mapping(pm_section)
|
|
124
|
+
acceptance = (
|
|
125
|
+
pm_map.get("acceptance_range") if isinstance(pm_map, dict) else None
|
|
126
|
+
)
|
|
127
|
+
if isinstance(acceptance, dict):
|
|
128
|
+
if acceptance.get("min") is not None:
|
|
129
|
+
try:
|
|
130
|
+
cfg_min = float(acceptance["min"])
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
cfg_min = None
|
|
133
|
+
if acceptance.get("max") is not None:
|
|
134
|
+
try:
|
|
135
|
+
cfg_max = float(acceptance["max"])
|
|
136
|
+
except (TypeError, ValueError):
|
|
137
|
+
cfg_max = None
|
|
138
|
+
except Exception:
|
|
139
|
+
cfg_min = None
|
|
140
|
+
cfg_max = None
|
|
141
|
+
|
|
142
|
+
def _parse_env(name: str) -> float | None:
|
|
143
|
+
try:
|
|
144
|
+
raw = os.environ.get(name, "")
|
|
145
|
+
if raw is None or str(raw).strip() == "":
|
|
146
|
+
return None
|
|
147
|
+
return float(raw)
|
|
148
|
+
except Exception:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
env_min = _parse_env("INVARLOCK_PM_ACCEPTANCE_MIN")
|
|
152
|
+
env_max = _parse_env("INVARLOCK_PM_ACCEPTANCE_MAX")
|
|
153
|
+
|
|
154
|
+
has_explicit = any(v is not None for v in (cfg_min, cfg_max, env_min, env_max))
|
|
155
|
+
if not has_explicit:
|
|
156
|
+
return {}
|
|
157
|
+
|
|
158
|
+
min_val = (
|
|
159
|
+
env_min if env_min is not None else cfg_min if cfg_min is not None else base_min
|
|
160
|
+
)
|
|
161
|
+
max_val = (
|
|
162
|
+
env_max if env_max is not None else cfg_max if cfg_max is not None else base_max
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
if min_val is not None and min_val <= 0:
|
|
167
|
+
min_val = base_min
|
|
168
|
+
except Exception:
|
|
169
|
+
min_val = base_min
|
|
170
|
+
try:
|
|
171
|
+
if max_val is not None and max_val <= 0:
|
|
172
|
+
max_val = base_max
|
|
173
|
+
except Exception:
|
|
174
|
+
max_val = base_max
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
if max_val is not None and min_val is not None and max_val < min_val:
|
|
178
|
+
max_val = min_val
|
|
179
|
+
except Exception:
|
|
180
|
+
max_val = base_max
|
|
181
|
+
|
|
182
|
+
return {"min": float(min_val), "max": float(max_val)}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _free_model_memory(model: object | None) -> None:
|
|
186
|
+
"""Best-effort cleanup to release GPU memory for a model object."""
|
|
187
|
+
if model is None:
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
import gc
|
|
191
|
+
|
|
192
|
+
del model
|
|
193
|
+
gc.collect()
|
|
194
|
+
if torch is not None and torch.cuda.is_available():
|
|
195
|
+
torch.cuda.empty_cache()
|
|
196
|
+
torch.cuda.synchronize()
|
|
197
|
+
except Exception:
|
|
198
|
+
# Cleanup should never raise; fallback is to proceed without cache purge
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _should_measure_overhead(profile_normalized: str) -> tuple[bool, bool]:
|
|
203
|
+
"""Return (measure_guard_overhead, skip_overhead) derived from env/profile."""
|
|
204
|
+
|
|
205
|
+
skip_overhead_env = (
|
|
206
|
+
os.environ.get("INVARLOCK_SKIP_OVERHEAD_CHECK", "").strip().lower()
|
|
207
|
+
)
|
|
208
|
+
skip_overhead = skip_overhead_env in {"1", "true", "yes"}
|
|
209
|
+
measure_guard_overhead = (
|
|
210
|
+
profile_normalized in {"ci", "release"} and not skip_overhead
|
|
211
|
+
)
|
|
212
|
+
return measure_guard_overhead, skip_overhead
|
|
213
|
+
|
|
214
|
+
|
|
84
215
|
def _choose_dataset_split(
|
|
85
216
|
*, requested: str | None, available: list[str] | None
|
|
86
217
|
) -> tuple[str, bool]:
|
|
@@ -1671,6 +1802,7 @@ def run_command(
|
|
|
1671
1802
|
"edit": edit_meta,
|
|
1672
1803
|
"guards": guard_metadata,
|
|
1673
1804
|
}
|
|
1805
|
+
pm_acceptance_range = _resolve_pm_acceptance_range(cfg)
|
|
1674
1806
|
|
|
1675
1807
|
console.print(f"🔌 Adapter: {adapter.name}")
|
|
1676
1808
|
|
|
@@ -1746,6 +1878,10 @@ def run_command(
|
|
|
1746
1878
|
"plugins": plugin_provenance,
|
|
1747
1879
|
"run_id": run_id,
|
|
1748
1880
|
}
|
|
1881
|
+
run_context.setdefault("primary_metric", {})["acceptance_range"] = (
|
|
1882
|
+
pm_acceptance_range
|
|
1883
|
+
)
|
|
1884
|
+
run_context["pm_acceptance_range"] = pm_acceptance_range
|
|
1749
1885
|
run_context["model_profile"] = {
|
|
1750
1886
|
"family": model_profile.family,
|
|
1751
1887
|
"default_loss": model_profile.default_loss,
|
|
@@ -2756,18 +2892,26 @@ def run_command(
|
|
|
2756
2892
|
|
|
2757
2893
|
restore_fn = _restore2
|
|
2758
2894
|
else:
|
|
2759
|
-
# reload path
|
|
2895
|
+
# reload path - properly free GPU memory before setting to None
|
|
2896
|
+
_free_model_memory(model)
|
|
2760
2897
|
model = None
|
|
2761
2898
|
restore_fn = None
|
|
2762
2899
|
except Exception:
|
|
2763
2900
|
# On any failure, fall back to reload-per-attempt path
|
|
2901
|
+
_free_model_memory(model)
|
|
2764
2902
|
model = None
|
|
2765
2903
|
restore_fn = None
|
|
2766
2904
|
|
|
2767
2905
|
# RETRY LOOP - All report processing inside loop
|
|
2768
2906
|
attempt = 1
|
|
2769
2907
|
profile_normalized = (profile or "").lower()
|
|
2770
|
-
measure_guard_overhead =
|
|
2908
|
+
measure_guard_overhead, skip_overhead = _should_measure_overhead(
|
|
2909
|
+
profile_normalized
|
|
2910
|
+
)
|
|
2911
|
+
if skip_overhead and profile_normalized in {"ci", "release"}:
|
|
2912
|
+
console.print(
|
|
2913
|
+
"[yellow]⚠️ Overhead check skipped via INVARLOCK_SKIP_OVERHEAD_CHECK[/yellow]"
|
|
2914
|
+
)
|
|
2771
2915
|
|
|
2772
2916
|
while True:
|
|
2773
2917
|
# Reset RNG streams each attempt to guarantee determinism across retries
|
|
@@ -2933,6 +3077,8 @@ def run_command(
|
|
|
2933
3077
|
if env_flags:
|
|
2934
3078
|
meta_payload["env_flags"] = env_flags
|
|
2935
3079
|
report["meta"].update(meta_payload)
|
|
3080
|
+
if pm_acceptance_range:
|
|
3081
|
+
report["meta"]["pm_acceptance_range"] = pm_acceptance_range
|
|
2936
3082
|
report["meta"]["model_profile"] = {
|
|
2937
3083
|
"family": model_profile.family,
|
|
2938
3084
|
"default_loss": model_profile.default_loss,
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
# mypy: ignore-errors
|
|
14
14
|
import copy
|
|
15
15
|
import hashlib
|
|
16
|
+
import inspect
|
|
16
17
|
import json
|
|
17
18
|
import math
|
|
18
19
|
import os
|
|
@@ -1322,24 +1323,36 @@ def make_certificate(
|
|
|
1322
1323
|
capacity_tokens = None
|
|
1323
1324
|
capacity_examples = None
|
|
1324
1325
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
auto.get("
|
|
1333
|
-
|
|
1334
|
-
|
|
1326
|
+
pm_acceptance_range = _resolve_pm_acceptance_range_from_report(report)
|
|
1327
|
+
|
|
1328
|
+
validation_kwargs = {
|
|
1329
|
+
"ppl": ppl_analysis,
|
|
1330
|
+
"spectral": spectral,
|
|
1331
|
+
"rmt": rmt,
|
|
1332
|
+
"invariants": invariants,
|
|
1333
|
+
"tier": auto.get("tier", "balanced"),
|
|
1334
|
+
"_ppl_metrics": ppl_metrics,
|
|
1335
|
+
"target_ratio": auto.get("target_pm_ratio"),
|
|
1336
|
+
"guard_overhead": guard_overhead_section,
|
|
1337
|
+
"primary_metric": report.get("metrics", {}).get("primary_metric")
|
|
1335
1338
|
if isinstance(report.get("metrics"), dict)
|
|
1336
1339
|
else None,
|
|
1337
|
-
moe_section,
|
|
1338
|
-
{
|
|
1340
|
+
"moe": moe_section,
|
|
1341
|
+
"dataset_capacity": {
|
|
1339
1342
|
"tokens_available": capacity_tokens,
|
|
1340
1343
|
"examples_available": capacity_examples,
|
|
1341
1344
|
},
|
|
1342
|
-
|
|
1345
|
+
}
|
|
1346
|
+
try:
|
|
1347
|
+
if (
|
|
1348
|
+
"pm_acceptance_range"
|
|
1349
|
+
in inspect.signature(_compute_validation_flags).parameters
|
|
1350
|
+
):
|
|
1351
|
+
validation_kwargs["pm_acceptance_range"] = pm_acceptance_range
|
|
1352
|
+
except Exception: # pragma: no cover - defensive against patched functions
|
|
1353
|
+
validation_kwargs["pm_acceptance_range"] = pm_acceptance_range
|
|
1354
|
+
|
|
1355
|
+
validation_flags = _compute_validation_flags(**validation_kwargs)
|
|
1343
1356
|
# Enforce validation key allow-list to prevent surface drift
|
|
1344
1357
|
_allowed_validation = _load_validation_allowlist()
|
|
1345
1358
|
validation_filtered = {
|
|
@@ -2537,6 +2550,103 @@ def _build_provenance_block(
|
|
|
2537
2550
|
return provenance
|
|
2538
2551
|
|
|
2539
2552
|
|
|
2553
|
+
def _resolve_pm_acceptance_range_from_report(
|
|
2554
|
+
report: dict[str, Any] | None,
|
|
2555
|
+
) -> dict[str, float]:
|
|
2556
|
+
"""Resolve primary-metric acceptance bounds from report context/meta/env."""
|
|
2557
|
+
|
|
2558
|
+
base_min = 0.95
|
|
2559
|
+
base_max = 1.10
|
|
2560
|
+
|
|
2561
|
+
def _safe_float(val: Any) -> float | None:
|
|
2562
|
+
try:
|
|
2563
|
+
if val is None:
|
|
2564
|
+
return None
|
|
2565
|
+
return float(val)
|
|
2566
|
+
except Exception:
|
|
2567
|
+
return None
|
|
2568
|
+
|
|
2569
|
+
cfg_min = None
|
|
2570
|
+
cfg_max = None
|
|
2571
|
+
ctx = report.get("context") if isinstance(report, dict) else None
|
|
2572
|
+
if isinstance(ctx, dict):
|
|
2573
|
+
pm_ctx = (
|
|
2574
|
+
ctx.get("primary_metric")
|
|
2575
|
+
if isinstance(ctx.get("primary_metric"), dict)
|
|
2576
|
+
else {}
|
|
2577
|
+
)
|
|
2578
|
+
if isinstance(pm_ctx, dict):
|
|
2579
|
+
cfg_min = _safe_float(pm_ctx.get("acceptance_range", {}).get("min"))
|
|
2580
|
+
cfg_max = _safe_float(pm_ctx.get("acceptance_range", {}).get("max"))
|
|
2581
|
+
if cfg_min is None or cfg_max is None:
|
|
2582
|
+
alt = ctx.get("pm_acceptance_range")
|
|
2583
|
+
if isinstance(alt, dict):
|
|
2584
|
+
cfg_min = (
|
|
2585
|
+
cfg_min if cfg_min is not None else _safe_float(alt.get("min"))
|
|
2586
|
+
)
|
|
2587
|
+
cfg_max = (
|
|
2588
|
+
cfg_max if cfg_max is not None else _safe_float(alt.get("max"))
|
|
2589
|
+
)
|
|
2590
|
+
|
|
2591
|
+
if (cfg_min is None or cfg_max is None) and isinstance(report, dict):
|
|
2592
|
+
meta = report.get("meta")
|
|
2593
|
+
if isinstance(meta, dict):
|
|
2594
|
+
meta_range = meta.get("pm_acceptance_range")
|
|
2595
|
+
if isinstance(meta_range, dict):
|
|
2596
|
+
cfg_min = (
|
|
2597
|
+
cfg_min
|
|
2598
|
+
if cfg_min is not None
|
|
2599
|
+
else _safe_float(meta_range.get("min"))
|
|
2600
|
+
)
|
|
2601
|
+
cfg_max = (
|
|
2602
|
+
cfg_max
|
|
2603
|
+
if cfg_max is not None
|
|
2604
|
+
else _safe_float(meta_range.get("max"))
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
def _parse_env(name: str) -> float | None:
|
|
2608
|
+
try:
|
|
2609
|
+
raw = os.environ.get(name, "")
|
|
2610
|
+
if raw is None or str(raw).strip() == "":
|
|
2611
|
+
return None
|
|
2612
|
+
return float(raw)
|
|
2613
|
+
except Exception:
|
|
2614
|
+
return None
|
|
2615
|
+
|
|
2616
|
+
env_min = _parse_env("INVARLOCK_PM_ACCEPTANCE_MIN")
|
|
2617
|
+
env_max = _parse_env("INVARLOCK_PM_ACCEPTANCE_MAX")
|
|
2618
|
+
|
|
2619
|
+
has_explicit = any(v is not None for v in (cfg_min, cfg_max, env_min, env_max))
|
|
2620
|
+
if not has_explicit:
|
|
2621
|
+
return {}
|
|
2622
|
+
|
|
2623
|
+
min_val = (
|
|
2624
|
+
env_min if env_min is not None else cfg_min if cfg_min is not None else base_min
|
|
2625
|
+
)
|
|
2626
|
+
max_val = (
|
|
2627
|
+
env_max if env_max is not None else cfg_max if cfg_max is not None else base_max
|
|
2628
|
+
)
|
|
2629
|
+
|
|
2630
|
+
try:
|
|
2631
|
+
if min_val is not None and min_val <= 0:
|
|
2632
|
+
min_val = base_min
|
|
2633
|
+
except Exception:
|
|
2634
|
+
min_val = base_min
|
|
2635
|
+
try:
|
|
2636
|
+
if max_val is not None and max_val <= 0:
|
|
2637
|
+
max_val = base_max
|
|
2638
|
+
except Exception:
|
|
2639
|
+
max_val = base_max
|
|
2640
|
+
|
|
2641
|
+
try:
|
|
2642
|
+
if max_val is not None and min_val is not None and max_val < min_val:
|
|
2643
|
+
max_val = min_val
|
|
2644
|
+
except Exception:
|
|
2645
|
+
max_val = base_max
|
|
2646
|
+
|
|
2647
|
+
return {"min": float(min_val), "max": float(max_val)}
|
|
2648
|
+
|
|
2649
|
+
|
|
2540
2650
|
def _compute_validation_flags(
|
|
2541
2651
|
ppl: dict[str, Any],
|
|
2542
2652
|
spectral: dict[str, Any],
|
|
@@ -2549,6 +2659,7 @@ def _compute_validation_flags(
|
|
|
2549
2659
|
primary_metric: dict[str, Any] | None = None,
|
|
2550
2660
|
moe: dict[str, Any] | None = None,
|
|
2551
2661
|
dataset_capacity: dict[str, Any] | None = None,
|
|
2662
|
+
pm_acceptance_range: dict[str, float] | None = None,
|
|
2552
2663
|
) -> dict[str, bool]:
|
|
2553
2664
|
"""Compute validation flags for the certificate including canonical gates."""
|
|
2554
2665
|
tier = (tier or "balanced").lower()
|
|
@@ -2569,7 +2680,25 @@ def _compute_validation_flags(
|
|
|
2569
2680
|
"aggressive": 1.20,
|
|
2570
2681
|
"none": 1.10,
|
|
2571
2682
|
}
|
|
2572
|
-
|
|
2683
|
+
acceptance = pm_acceptance_range if isinstance(pm_acceptance_range, dict) else {}
|
|
2684
|
+
ratio_min_bound = None
|
|
2685
|
+
ratio_max_bound = None
|
|
2686
|
+
try:
|
|
2687
|
+
if acceptance.get("min") is not None:
|
|
2688
|
+
ratio_min_bound = float(acceptance.get("min"))
|
|
2689
|
+
except Exception:
|
|
2690
|
+
ratio_min_bound = None
|
|
2691
|
+
try:
|
|
2692
|
+
if acceptance.get("max") is not None:
|
|
2693
|
+
ratio_max_bound = float(acceptance.get("max"))
|
|
2694
|
+
except Exception:
|
|
2695
|
+
ratio_max_bound = None
|
|
2696
|
+
|
|
2697
|
+
ratio_limit = (
|
|
2698
|
+
ratio_max_bound
|
|
2699
|
+
if isinstance(ratio_max_bound, (int | float)) and math.isfinite(ratio_max_bound)
|
|
2700
|
+
else tier_thresholds.get(tier, 1.10)
|
|
2701
|
+
)
|
|
2573
2702
|
if isinstance(target_ratio, int | float) and target_ratio > 0:
|
|
2574
2703
|
ratio_limit = min(ratio_limit, float(target_ratio))
|
|
2575
2704
|
|
|
@@ -2636,9 +2765,18 @@ def _compute_validation_flags(
|
|
|
2636
2765
|
tokens_ok_eff = tokens_ok or _tiny_relax
|
|
2637
2766
|
# Apply hysteresis to ratio limit if needed
|
|
2638
2767
|
ratio_limit_with_hyst = ratio_limit + max(0.0, hysteresis_ratio)
|
|
2768
|
+
lower_bound_ok = True
|
|
2769
|
+
if ratio_min_bound is not None and isinstance(ratio_vs_baseline, (int | float)):
|
|
2770
|
+
try:
|
|
2771
|
+
lower_bound_ok = math.isfinite(float(ratio_vs_baseline)) and (
|
|
2772
|
+
float(ratio_vs_baseline) >= float(ratio_min_bound)
|
|
2773
|
+
)
|
|
2774
|
+
except Exception:
|
|
2775
|
+
lower_bound_ok = True
|
|
2639
2776
|
compression_acceptable = (
|
|
2640
2777
|
isinstance(ratio_vs_baseline, int | float)
|
|
2641
2778
|
and math.isfinite(ratio_vs_baseline)
|
|
2779
|
+
and lower_bound_ok
|
|
2642
2780
|
and ratio_vs_baseline <= ratio_limit_with_hyst
|
|
2643
2781
|
and tokens_ok_eff
|
|
2644
2782
|
)
|
|
@@ -2655,7 +2793,9 @@ def _compute_validation_flags(
|
|
|
2655
2793
|
and all(isinstance(x, int | float) and math.isfinite(x) for x in ratio_ci)
|
|
2656
2794
|
):
|
|
2657
2795
|
compression_acceptable = (
|
|
2658
|
-
compression_acceptable
|
|
2796
|
+
compression_acceptable
|
|
2797
|
+
and ratio_ci[1] <= ratio_limit_with_hyst
|
|
2798
|
+
and (ratio_min_bound is None or ratio_ci[0] >= ratio_min_bound)
|
|
2659
2799
|
)
|
|
2660
2800
|
|
|
2661
2801
|
# 3. RMT ε-rule compliance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invarlock
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Edit‑agnostic robustness certificates for weight edits (InvarLock framework)
|
|
5
5
|
Author-email: InvarLock Team <oss@invarlock.dev>
|
|
6
6
|
Maintainer-email: InvarLock Maintainers <support@invarlock.dev>
|
|
@@ -112,7 +112,7 @@ they don’t, roll back safely.
|
|
|
112
112
|
Technical: edit‑agnostic guard pipeline (invariants → spectral → RMT →
|
|
113
113
|
variance) producing a machine‑readable Safety Certificate.
|
|
114
114
|
|
|
115
|
-
> **Status:** 0.3.
|
|
115
|
+
> **Status:** 0.3.1 (pre‑1.0). Until 1.0, **minor** releases may be
|
|
116
116
|
> breaking. See CLI help and the CHANGELOG for updates.
|
|
117
117
|
|
|
118
118
|
[](https://github.com/invarlock/invarlock/actions/workflows/ci.yml)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|