"""Clock-phase normal envelopes frozen before replay; never inspect future readings.""" from datetime import datetime from math import isfinite CALIBRATION_VERSION = "scada-monitor-calibration@2" DIAGNOSTIC_POLICY = "normal-phase-envelope@1" def weighted_diagnosis(document: dict) -> bool: return (document.get("monitoring_config") or {}).get("decision_policy") == "weighted" def current_calibration(document: dict, start: datetime) -> dict | None: calibration = document.get("monitoring_calibration") or {} if (calibration.get("version") == CALIBRATION_VERSION and calibration.get("diagnostic_policy") == DIAGNOSTIC_POLICY and calibration.get("knowledge_cutoff") == start.isoformat()): return calibration return None def pollution_signal(document: dict, event: dict) -> bool: """Water-quality measurements have their own incident investigation workflow.""" if event.get("category") == "data_quality": return False devices = {item["device_id"]: item.get("device_type") for item in document.get("devices", [])} ids = event.get("device_ids") or next((item.get("device_ids", []) for item in (document.get("config") or {}).get("rules", []) if item["rule_id"] == event.get("rule_id")), []) return bool(ids) and all(devices.get(device) in {"quality", "water_quality"} for device in ids) def diagnostic_signal(document: dict, event: dict) -> bool: """Never revive a persisted fixed-percentage event as new diagnosis evidence.""" if event.get("category") == "data_quality" or pollution_signal(document, event): return False if not weighted_diagnosis(document): return True rules = (document.get("config") or {}).get("rules", []) rule = next((item for item in rules if item["rule_id"] == event["rule_id"]), None) return bool(rule and (rule["kind"] == "bounds" or event.get("diagnostic_policy") == DIAGNOSTIC_POLICY)) def normal_profile_bounds(profile: dict | None, at: datetime) -> tuple[float, float] | None: if not profile: return None interval = profile.get("interval_seconds", 0) if not profile.get("phase_origin"): return None origin = datetime.fromisoformat(profile["phase_origin"]) lower, upper = profile.get("lower", []), profile.get("upper", []) offset = (at - origin).total_seconds() if interval <= 0 or offset < 0 or offset % interval or not lower or len(lower) != len(upper): return None slot = int(offset // interval) % len(lower) low, high = lower[slot], upper[slot] if low is None or high is None or not isfinite(low) or not isfinite(high): return None return float(low), float(high)