"""Explicit, retrospective equal-endpoint estimates for sparse state publication. Never changes a source timestamp, real-time freshness limit, meter evidence or actuator lease. A repeated value after a bounded gap supports a MODEL estimate, not proof that the physical signal was constant between the observations. """ from math import isfinite import re def validate_policy(policy, sources): if policy is None: return None if (not isinstance(policy, dict) or set(policy) != {'version', 'method', 'sources'} or type(policy['version']) is not int or policy['version'] != 1 or policy['method'] != 'equal_endpoint_v1' or not isinstance(policy['sources'], dict) or not policy['sources']): raise ValueError('Explicit historical timing policy required') defined = {s['key']: s for s in sources} for key, rule in policy['sources'].items(): if key not in defined or defined[key]['role'] not in ('pv', 'physical_storage', 'solar_scale'): raise ValueError('Timing estimates limited to explicit power/scale sources') if not isinstance(rule, dict) or set(rule) != {'maxSpanSeconds', 'evidenceId'}: raise ValueError('Timing bound and evidence reference required') if type(rule['maxSpanSeconds']) is not int or not defined[key]['maxAgeSeconds'] <= rule['maxSpanSeconds'] <= 180: raise ValueError('Historical endpoint span outside reviewed bounds') if not isinstance(rule['evidenceId'], str) or not re.fullmatch(r'[A-Za-z0-9_-]{8,100}', rule['evidenceId']): raise ValueError('Invalid publication evidence reference') return policy def endpoint_bridges(series, first_observed, blocks, capture_gaps, policy, source_specs): """Index strictly consecutive, equal original timestamps; never invent endpoints. Only the stale tail of a normal hold is filled. A missing/invalid capture or a source conflict forbids bridging. The right endpoint must have been observed; its availability is retained by the caller for causal training and replay. """ result = {key: {} for key in series} if policy is None: return result for key, rule in policy['sources'].items(): if key not in series: continue # e.g. an unused DC channel in an AC-terminal formula values = series[key] times = sorted(values) for left, right in zip(times, times[1:]): value, next_value = values[left], values[right] if (value is None or next_value is None or type(value) not in (int, float) or not isfinite(value) or value != next_value): continue if not source_specs[key]['maxAgeSeconds'] < right-left <= rule['maxSpanSeconds']: continue if any(start < right and end > left for start, end in (*blocks[key], *capture_gaps)): continue result[key][left] = {'end': right, 'availableAt': first_observed[key][right], 'spanSeconds': right-left} return result