[AZ-220] Add shared runtime contract models

Implement the shared DTO contract surface with validation so runtime components consume one public model set instead of duplicating shapes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-03 13:22:50 +03:00
parent 72a9df6b57
commit 5156453224
8 changed files with 361 additions and 3 deletions
+26 -1
View File
@@ -1,3 +1,28 @@
"""Shared DTO and interface contract namespace."""
CONTRACT_VERSION = "0.1.0"
from shared.contracts.models import (
AnchorDecision,
CacheTileRecord,
FdrEvent,
FramePacket,
PositionEstimate,
RuntimeContractModel,
TelemetrySample,
VioStatePacket,
VprCandidate,
)
CONTRACT_VERSION = "1.0.0"
__all__ = [
"AnchorDecision",
"CONTRACT_VERSION",
"CacheTileRecord",
"FdrEvent",
"FramePacket",
"PositionEstimate",
"RuntimeContractModel",
"TelemetrySample",
"VioStatePacket",
"VprCandidate",
]
+116
View File
@@ -0,0 +1,116 @@
"""Shared runtime DTO contracts.
These models intentionally carry only cross-component shape and validation rules.
Component algorithms and storage choices stay in their owning packages.
"""
from datetime import date
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt, PositiveFloat
from pydantic import model_validator
class RuntimeContractModel(BaseModel):
"""Base settings for public runtime contracts."""
model_config = ConfigDict(extra="forbid", frozen=True)
class FramePacket(RuntimeContractModel):
frame_id: str = Field(min_length=1)
timestamp_ns: NonNegativeInt
image_ref: str = Field(min_length=1)
calibration_id: str = Field(min_length=1)
occlusion: Literal["clear", "partial", "total", "unreadable"]
quality: float = Field(ge=0.0, le=1.0)
normalization_hint: str | None = None
raw_frame_retained: bool = False
@model_validator(mode="after")
def raw_frames_must_not_be_retained(self) -> "FramePacket":
if self.raw_frame_retained:
raise ValueError("raw frame payloads must be referenced, not retained")
return self
class TelemetrySample(RuntimeContractModel):
timestamp_ns: NonNegativeInt
imu: dict[str, float]
attitude: dict[str, float]
altitude_m: float
airspeed_mps: NonNegativeFloat
gps_health: Literal["healthy", "degraded", "lost", "spoofed"]
class VioStatePacket(RuntimeContractModel):
timestamp_ns: NonNegativeInt
relative_pose: dict[str, float]
velocity_mps: tuple[float, float, float]
bias_estimate: dict[str, float] | None = None
tracking_quality: float = Field(ge=0.0, le=1.0)
covariance_hint: list[list[float]] | None = None
class PositionEstimate(RuntimeContractModel):
timestamp_ns: NonNegativeInt
latitude_deg: float = Field(ge=-90.0, le=90.0)
longitude_deg: float = Field(ge=-180.0, le=180.0)
altitude_m: float
covariance_semimajor_m: NonNegativeFloat
source_label: Literal["satellite_anchored", "vo_extrapolated", "dead_reckoned", "no_fix"]
fix_type: int = Field(ge=0, le=3)
horizontal_accuracy_m: NonNegativeFloat
anchor_age_ms: NonNegativeInt
@model_validator(mode="after")
def accuracy_must_not_under_report_covariance(self) -> "PositionEstimate":
if self.horizontal_accuracy_m < self.covariance_semimajor_m:
raise ValueError("horizontal_accuracy_m must not under-report covariance_semimajor_m")
return self
class VprCandidate(RuntimeContractModel):
chunk_id: str = Field(min_length=1)
tile_id: str = Field(min_length=1)
score: float = Field(ge=0.0, le=1.0)
footprint: dict[str, float]
freshness_status: Literal["fresh", "stale", "rejected"]
class AnchorDecision(RuntimeContractModel):
candidate_id: str = Field(min_length=1)
accepted: bool
estimated_pose: dict[str, float] | None = None
inliers: NonNegativeInt
mean_reprojection_error_px: NonNegativeFloat
rejection_reason: str | None = None
@model_validator(mode="after")
def accepted_anchors_require_pose(self) -> "AnchorDecision":
if self.accepted and self.estimated_pose is None:
raise ValueError("accepted anchor decisions require estimated_pose")
if self.accepted and self.rejection_reason is not None:
raise ValueError("accepted anchor decisions must not include rejection_reason")
return self
class CacheTileRecord(RuntimeContractModel):
tile_id: str = Field(min_length=1)
crs: str = Field(min_length=1)
meters_per_pixel: PositiveFloat
capture_date: date
signature_hash: str = Field(min_length=1)
trust_level: Literal["trusted", "generated", "quarantined", "rejected"]
freshness_status: Literal["fresh", "stale", "rejected"]
provenance: str = Field(min_length=1)
class FdrEvent(RuntimeContractModel):
event_type: str = Field(min_length=1)
timestamp_ns: NonNegativeInt
component: str = Field(min_length=1)
severity: Literal["debug", "info", "warning", "error", "critical"]
payload_ref: str = Field(min_length=1)
mission_id: str = Field(min_length=1)
run_id: str = Field(min_length=1)