probes/_zscore: refuse when stats['degenerate'] is set (F02)
- SHA
15d4314fc2940a66490307e9b2576bbf130c9003- Parents
-
edd5a03 - Tree
8321e2a
15d4314
15d4314fc2940a66490307e9b2576bbf130c9003edd5a03
8321e2a| Status | File | + | - |
|---|---|---|---|
| M |
src/dlm_sway/probes/_zscore.py
|
9 | 1 |
src/dlm_sway/probes/_zscore.pymodified@@ -39,7 +39,11 @@ def z_score(raw: float, stats: Mapping[str, float] | None) -> float | None: | ||
| 39 | 39 | Returns ``None`` when: |
| 40 | 40 | |
| 41 | 41 | - ``stats`` is missing (no calibration ran for this kind) |
| 42 | - - ``std`` is below :data:`MIN_STD` (degenerate null distribution) | |
| 42 | + - ``stats["degenerate"]`` is truthy (F02 Audit 03 — null ran but | |
| 43 | + was too narrow to calibrate against: ``runs: 1``, or multi-seed | |
| 44 | + raws that collapsed to an effectively-zero variance) | |
| 45 | + - ``std`` is below :data:`MIN_STD` (belt-and-suspenders guard | |
| 46 | + for stats dicts that predate the ``degenerate`` field) | |
| 43 | 47 | - ``raw`` or ``mean`` is non-finite |
| 44 | 48 | |
| 45 | 49 | Callers that get ``None`` are expected to fall back to their probe's |
@@ -54,6 +58,10 @@ def z_score(raw: float, stats: Mapping[str, float] | None) -> float | None: | ||
| 54 | 58 | return None |
| 55 | 59 | if not (math.isfinite(raw) and math.isfinite(mean) and math.isfinite(std)): |
| 56 | 60 | return None |
| 61 | + # ``degenerate`` is stored as a float (1.0 / 0.0) so the stats | |
| 62 | + # dict stays Mapping[str, float] across every consumer. | |
| 63 | + if stats.get("degenerate", 0.0) >= 0.5: | |
| 64 | + return None | |
| 57 | 65 | if std < MIN_STD: |
| 58 | 66 | return None |
| 59 | 67 | return float((raw - mean) / std) |