"""Stage-1, transport-neutral contract for stream-first perception. These executable invariants do not start models, replace the legacy queue, or claim that a stream transport exists. Replay and live must implement the same contract in stage 2. Existing perception objects remain the domain vocabulary. """ from __future__ import annotations import math import re from dataclasses import asdict, dataclass from typing import Final, cast STREAM_START_SCHEMA: Final = "missioncore.perception-stream-start/v1" REALTIME_CONTRACT_SCHEMA: Final = "missioncore.perception-realtime-contract/v1" REQUIRED_CHANNELS: Final = ("camera", "point-cloud", "pose") REQUIRED_LAYERS: Final = ("segmentation", "objects", "geometry", "motion", "costmap", "policy") _ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$") _SHA: Final = re.compile(r"^[a-f0-9]{64}$") class RealtimeContractError(ValueError): """The caller attempted an ambiguous, unbounded or non-causal contract.""" def _integer(value: object, name: str, *, minimum: int = 0) -> int: if type(value) is not int or value < minimum: raise RealtimeContractError(f"{name} must be an integer >= {minimum}") return value def _number(value: object, name: str, *, minimum: float = 0.0) -> float: if type(value) not in (int, float): raise RealtimeContractError(f"{name} must be a finite number") number = float(cast(int | float, value)) if not math.isfinite(number) or number < minimum: raise RealtimeContractError(f"{name} must be finite and >= {minimum}") return number def _identifier(value: object, name: str) -> None: if not isinstance(value, str) or _ID.fullmatch(value) is None: raise RealtimeContractError(f"{name} is invalid") def _digest(value: object, name: str) -> None: if not isinstance(value, str) or _SHA.fullmatch(value) is None: raise RealtimeContractError(f"{name} must be a SHA-256 identity") @dataclass(frozen=True, slots=True) class StreamStart: """Bounded handshake; deliberately no source duration, EOF or file inventory. The epoch is reset on reconnect. Source hashes belong to chunk admission and the asynchronous final receipt, not a full-file startup verification. Calibration/config references resolve to bounded, authenticated metadata. """ run_id: str source_id: str worker_id: str epoch_id: str lease_generation: int profile_sha256: str image_sha256: str effective_config_sha256: str calibration_sha256: str clock_domain_id: str input_mode: str channels: tuple[str, ...] = REQUIRED_CHANNELS def __post_init__(self) -> None: for name in ("run_id", "source_id", "worker_id", "epoch_id", "clock_domain_id"): _identifier(getattr(self, name), name) _integer(self.lease_generation, "lease_generation", minimum=1) for name in ( "profile_sha256", "image_sha256", "effective_config_sha256", "calibration_sha256", ): _digest(getattr(self, name), name) if self.input_mode not in ("recorded-source-paced", "live"): raise RealtimeContractError("only source-paced replay and live are admitted") if self.channels != REQUIRED_CHANNELS: raise RealtimeContractError("the full profile requires camera, point-cloud and pose") def to_dict(self) -> dict[str, object]: value = asdict(self) value["schema_version"] = STREAM_START_SCHEMA value["channels"] = list(self.channels) return value @classmethod def from_dict(cls, value: object) -> StreamStart: if not isinstance(value, dict): raise RealtimeContractError("stream start must be an object") fields = set(cls.__dataclass_fields__) if set(value) != fields | {"schema_version"}: raise RealtimeContractError("stream start fields changed; full-source fields forbidden") if value["schema_version"] != STREAM_START_SCHEMA: raise RealtimeContractError("stream start schema changed") if not isinstance(value["channels"], list): raise RealtimeContractError("channels must be an array") arguments = {name: value[name] for name in fields} arguments["channels"] = tuple(value["channels"]) return cls(**arguments) @dataclass(frozen=True, slots=True) class RealtimeBudgets: """Preregistered engineering limits, never vehicle-safety thresholds.""" maximum_start_metadata_bytes: int maximum_chunk_bytes: int maximum_inflight_bytes: int maximum_pending_camera_frames: int maximum_codec_preroll_ms: float maximum_pose_age_ms: float maximum_clock_uncertainty_ms: float maximum_release_lag_ms: float maximum_output_age_p95_ms: float maximum_output_age_p99_ms: float maximum_layer_age_ms: float maximum_first_result_ms: float maximum_warmup_seconds: float maximum_stop_seconds: float maximum_vram_mib: int maximum_rss_mib: int maximum_backlog_growth_ms: float def __post_init__(self) -> None: for name in ( "maximum_start_metadata_bytes", "maximum_chunk_bytes", "maximum_inflight_bytes", "maximum_pending_camera_frames", "maximum_vram_mib", "maximum_rss_mib", ): _integer(getattr(self, name), name, minimum=1) for name in set(self.__dataclass_fields__) - { "maximum_start_metadata_bytes", "maximum_chunk_bytes", "maximum_inflight_bytes", "maximum_pending_camera_frames", "maximum_vram_mib", "maximum_rss_mib", }: _number(getattr(self, name), name, minimum=0.001) if self.maximum_chunk_bytes > self.maximum_inflight_bytes: raise RealtimeContractError("one chunk exceeds the inflight byte budget") if not ( self.maximum_output_age_p95_ms <= self.maximum_output_age_p99_ms <= self.maximum_layer_age_ms ): raise RealtimeContractError("output percentile and freshness budgets disagree") @classmethod def from_dict(cls, value: object) -> RealtimeBudgets: if not isinstance(value, dict) or set(value) != set(cls.__dataclass_fields__): raise RealtimeContractError("realtime budget fields changed") return cls(**value) @dataclass(frozen=True, slots=True) class LayerEvidence: """Freshness metadata wrapping existing domain payloads, not a new ontology. source_time_ns is mapped into the run's common source timeline. It is not a worker clock. A retained layer must keep its original timestamp/sequence. Network end-to-end age is measured separately, with clock uncertainty. """ layer: str epoch_id: str source_sequence: int | None source_time_ns: int | None state: str payload_sha256: str | None # Old callers omit this field. New layered results require it explicitly: # recomputing a derived layer cannot refresh the age of its required inputs. oldest_required_input_time_ns: int | None = None def __post_init__(self) -> None: if self.layer not in REQUIRED_LAYERS: raise RealtimeContractError("unknown full-profile layer") _identifier(self.epoch_id, "epoch_id") if self.state not in ("current", "held", "stale", "unavailable"): raise RealtimeContractError("unknown evidence currentness") if self.state == "unavailable": if any( x is not None for x in ( self.source_sequence, self.source_time_ns, self.payload_sha256, self.oldest_required_input_time_ns, ) ): raise RealtimeContractError("unavailable evidence cannot claim a payload") else: _integer(self.source_sequence, "source_sequence") _integer(self.source_time_ns, "source_time_ns") _digest(self.payload_sha256, "payload_sha256") if self.oldest_required_input_time_ns is not None: _integer(self.oldest_required_input_time_ns, "oldest_required_input_time_ns") if self.oldest_required_input_time_ns > cast(int, self.source_time_ns): raise RealtimeContractError("required input is newer than its layer") def validate_scene_layers( layers: tuple[LayerEvidence, ...], *, epoch_id: str, source_time_ns: int, maximum_layer_age_ms: float, ) -> None: """Reject missing layers, cross-epoch reuse, look-ahead and relabelled stale data. Explicit unavailable/stale output remains valid evidence, not permission to move. Deciding hard_surface/occupied precedence belongs to the existing policy evaluator, whose results cannot grant actuation. """ _identifier(epoch_id, "epoch_id") _integer(source_time_ns, "source_time_ns") _number(maximum_layer_age_ms, "maximum_layer_age_ms", minimum=0.001) if len(layers) != len(REQUIRED_LAYERS) or {x.layer for x in layers} != set(REQUIRED_LAYERS): raise RealtimeContractError("a scene must account for every required layer exactly once") for layer in layers: if layer.epoch_id != epoch_id: raise RealtimeContractError("layer belongs to another stream epoch") if layer.source_time_ns is None: continue if layer.source_time_ns > source_time_ns: raise RealtimeContractError("future observations are not causal input") stamp = layer.oldest_required_input_time_ns age_ns = source_time_ns - (layer.source_time_ns if stamp is None else stamp) if age_ns < 0: raise RealtimeContractError("future observations are not causal input") if age_ns > maximum_layer_age_ms * 1_000_000 and layer.state in ("current", "held"): raise RealtimeContractError("expired evidence cannot be current or held") @dataclass(frozen=True, slots=True) class ReplayMeasurements: """Measured whole-path receipt; component timings alone cannot populate it. All age figures include source release, transfer, decode, graph processing and delivery to the application. The slowest required layer determines full-scene age. Qualification is bound to identities by the caller. """ source_frames: int declared_source_gap_frames: int terminal_frames: int fresh_complete_scenes: int capacity_drops: int expired_frames: int failed_frames: int unexpected_unavailable_layers: int full_source_preloaded: bool results_before_end_of_source: bool replay_speed: float maximum_release_lag_ms: float output_age_p95_ms: float output_age_p99_ms: float maximum_layer_age_ms: float first_result_ms: float warmup_seconds: float stop_seconds: float peak_inflight_bytes: int peak_pending_camera_frames: int peak_vram_mib: int peak_rss_mib: int clock_uncertainty_ms: float backlog_growth_ms: float computed_layers: tuple[str, ...] def __post_init__(self) -> None: integer_fields = { "source_frames", "declared_source_gap_frames", "terminal_frames", "fresh_complete_scenes", "capacity_drops", "expired_frames", "failed_frames", "unexpected_unavailable_layers", "peak_inflight_bytes", "peak_pending_camera_frames", "peak_vram_mib", "peak_rss_mib", } boolean_fields = {"full_source_preloaded", "results_before_end_of_source"} for name in integer_fields: _integer(getattr(self, name), name) for name in boolean_fields: if type(getattr(self, name)) is not bool: raise RealtimeContractError(f"{name} must be boolean") for name in ( set(self.__dataclass_fields__) - integer_fields - boolean_fields - {"computed_layers"} ): _number(getattr(self, name), name) if ( self.source_frames == 0 or self.declared_source_gap_frames > self.source_frames or self.fresh_complete_scenes > self.terminal_frames ): raise RealtimeContractError("measurement frame counts are invalid") if self.output_age_p95_ms > self.output_age_p99_ms: raise RealtimeContractError("measurement percentiles are invalid") if not isinstance(self.computed_layers, tuple) or any( not isinstance(layer, str) for layer in self.computed_layers ): raise RealtimeContractError("computed layers must be an immutable tuple of names") if len(set(self.computed_layers)) != len(self.computed_layers): raise RealtimeContractError("computed layers are duplicated") def realtime_failures(value: ReplayMeasurements, limits: RealtimeBudgets) -> tuple[str, ...]: """Return every failed strict replay gate, not a product-ready flag. First baseline is every source camera frame, no silent stride. A later multirate profile needs its own explicit contract and measured qualification. Source gaps are bound to the source ledger, never inferred from model failures. They must still produce explicit degraded scenes and terminal accounting; no complete fresh-scene claim is allowed for those frames. """ failures: list[str] = [] for failed, reason in ( (value.full_source_preloaded, "full-source-preload"), (not value.results_before_end_of_source, "no-incremental-result"), (value.replay_speed != 1.0, "source-clock-not-1x"), (value.terminal_frames != value.source_frames, "incomplete-input-accounting"), ( value.fresh_complete_scenes != value.source_frames - value.declared_source_gap_frames, "incomplete-fresh-scenes", ), (value.fresh_complete_scenes == 0, "no-evaluable-scene"), (value.capacity_drops != 0, "capacity-drops"), (value.expired_frames != 0, "expired-frames"), (value.failed_frames != 0, "failed-frames"), (value.unexpected_unavailable_layers != 0, "required-layer-unavailable"), (set(value.computed_layers) != set(REQUIRED_LAYERS), "full-profile-not-computed"), ): if failed: failures.append(reason) for measurement, budget in ( ("maximum_release_lag_ms", "maximum_release_lag_ms"), ("output_age_p95_ms", "maximum_output_age_p95_ms"), ("output_age_p99_ms", "maximum_output_age_p99_ms"), ("maximum_layer_age_ms", "maximum_layer_age_ms"), ("first_result_ms", "maximum_first_result_ms"), ("warmup_seconds", "maximum_warmup_seconds"), ("stop_seconds", "maximum_stop_seconds"), ("peak_inflight_bytes", "maximum_inflight_bytes"), ("peak_pending_camera_frames", "maximum_pending_camera_frames"), ("peak_vram_mib", "maximum_vram_mib"), ("peak_rss_mib", "maximum_rss_mib"), ("clock_uncertainty_ms", "maximum_clock_uncertainty_ms"), ("backlog_growth_ms", "maximum_backlog_growth_ms"), ): if getattr(value, measurement) > getattr(limits, budget): failures.append(measurement) return tuple(failures)