feat(perception): establish object centric contracts

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 13:32:34 +03:00
parent 8ef215e98f
commit ae1e41f7fe
8 changed files with 2665 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Object-centric, source-neutral Mission Core perception product boundary."""
from .adapters import (
PerceptionAdapterError,
observations_from_track_geometry,
temporal_obstacles_from_e34_projection,
)
from .baseline import (
BASELINE_PROFILE_ID,
BASELINE_SCHEMA,
BaselineContractError,
BaselineProfile,
BaselineVerification,
load_m4_baseline,
validate_reuse_inventory,
verify_m4_baseline,
)
from .contracts import (
LOCAL_OBSTACLE_MAP_SCHEMA,
OBJECT_PROPOSAL_SCHEMA,
OBSTACLE_OBSERVATION_SCHEMA,
SOURCE_ENVELOPE_SCHEMA,
TEMPORAL_OBSTACLE_SCHEMA,
THREAT_ASSESSMENT_SCHEMA,
BoundingRegion2D,
ClockBasis,
CorridorIntersection,
EvidenceBasis,
EvidenceCurrentness,
FalseAuthority,
GridCell,
HistorySample,
LocalObstacleMap,
MetricGeometry,
ModalityOutcome,
ModalityStatus,
MotionState,
ObjectProposal2D,
ObstacleObservation,
PerceptionContractError,
QualificationState,
SourceAccounting,
SourceEnvelope,
TemporalObstacle,
TemporalState,
ThreatAssessment,
ThreatAuthority,
ThreatDecision,
TimestampBundle,
validate_exclusive_point_ownership,
)
from .providers import (
REFERENCE_GRAPH_CONFIG_SCHEMA,
DetectorProvider,
GeometryAssociationProvider,
GraphAuthority,
MotionProvider,
ProviderContractError,
ProviderPin,
ProviderRole,
QueuePolicy,
ReferencePerceptionGraphConfig,
SourceProvider,
TemporalStateProvider,
ThreatProvider,
)
__all__ = [
"BASELINE_PROFILE_ID",
"BASELINE_SCHEMA",
"BaselineContractError",
"BaselineProfile",
"BaselineVerification",
"load_m4_baseline",
"validate_reuse_inventory",
"verify_m4_baseline",
"PerceptionAdapterError",
"observations_from_track_geometry",
"temporal_obstacles_from_e34_projection",
"LOCAL_OBSTACLE_MAP_SCHEMA",
"OBJECT_PROPOSAL_SCHEMA",
"OBSTACLE_OBSERVATION_SCHEMA",
"SOURCE_ENVELOPE_SCHEMA",
"TEMPORAL_OBSTACLE_SCHEMA",
"THREAT_ASSESSMENT_SCHEMA",
"BoundingRegion2D",
"ClockBasis",
"CorridorIntersection",
"EvidenceBasis",
"EvidenceCurrentness",
"FalseAuthority",
"GridCell",
"HistorySample",
"LocalObstacleMap",
"MetricGeometry",
"ModalityOutcome",
"ModalityStatus",
"MotionState",
"ObjectProposal2D",
"ObstacleObservation",
"PerceptionContractError",
"QualificationState",
"SourceAccounting",
"SourceEnvelope",
"TemporalObstacle",
"TemporalState",
"ThreatAssessment",
"ThreatAuthority",
"ThreatDecision",
"TimestampBundle",
"validate_exclusive_point_ownership",
"REFERENCE_GRAPH_CONFIG_SCHEMA",
"DetectorProvider",
"GeometryAssociationProvider",
"GraphAuthority",
"MotionProvider",
"ProviderContractError",
"ProviderPin",
"ProviderRole",
"QueuePolicy",
"ReferencePerceptionGraphConfig",
"SourceProvider",
"TemporalStateProvider",
"ThreatProvider",
]
+274
View File
@@ -0,0 +1,274 @@
"""One-way adapters from admitted historical primitives to product contracts.
The adapters do not mutate TrackGeometry/E34 documents and never upgrade held,
persistent or missing evidence into current metric occupancy.
"""
from __future__ import annotations
import math
from collections.abc import Mapping
import numpy as np
from k1link.compute.temporal_occupied_layer import TemporalFrameProjection
from k1link.compute.track_geometry import (
TrackGeometryCurrentness,
TrackGeometryEvidenceState,
TrackGeometryFrame,
TrackGeometryMetricBasis,
TrackGeometryOwnerKind,
)
from .contracts import (
EvidenceBasis,
EvidenceCurrentness,
GridCell,
HistorySample,
MetricGeometry,
MotionState,
ObstacleObservation,
TemporalObstacle,
TemporalState,
validate_exclusive_point_ownership,
)
class PerceptionAdapterError(ValueError):
"""An admitted historical value cannot be represented without inventing evidence."""
def observations_from_track_geometry(
frame: TrackGeometryFrame,
*,
source_id: str,
frame_id: str,
evidence_time_ns: int,
proposal_ids_by_owner: Mapping[str, tuple[str, ...]] | None = None,
) -> tuple[ObstacleObservation, ...]:
"""Adapt TrackGeometry v1 while preserving exact point ownership and uncertainty."""
proposals = proposal_ids_by_owner or {}
observations: list[ObstacleObservation] = []
for geometry in frame.geometries:
source_indices = frame.point_slab.owned_source_indices(geometry.owner_key)
source_point_ids = tuple(int(value) for value in source_indices.tolist())
metric_geometry: MetricGeometry | None = None
currentness = _currentness(geometry.currentness)
occupied_support = False
if geometry.metric_basis is TrackGeometryMetricBasis.CURRENT_POINTS:
if not source_point_ids or geometry.range_m is None:
raise PerceptionAdapterError(
"current TrackGeometry lost its qualified point support"
)
owner_index = frame.point_slab.owner_keys.index(geometry.owner_key)
points = frame.point_slab.points_xyz_m[
frame.point_slab.owner_indices == owner_index
].astype(np.float64, copy=False)
centroid = points.mean(axis=0)
covariance = points.var(axis=0)
metric_geometry = MetricGeometry(
coordinate_frame=frame.binding.coordinate_frame,
centroid_xyz_m=(float(centroid[0]), float(centroid[1]), float(centroid[2])),
range_m=float(geometry.range_m),
covariance_diagonal_m2=(
float(covariance[0]),
float(covariance[1]),
float(covariance[2]),
),
)
occupied_support = True
else:
source_point_ids = ()
basis = _basis(geometry.evidence_state, geometry.owner_kind)
if basis in {EvidenceBasis.CAMERA, EvidenceBasis.CONFLICT}:
metric_geometry = None
source_point_ids = ()
occupied_support = False
if currentness is not EvidenceCurrentness.CURRENT:
metric_geometry = None
source_point_ids = ()
occupied_support = False
reason_codes = list(geometry.reason_codes)
if geometry.currentness is TrackGeometryCurrentness.PERSISTENT:
reason_codes.append("persistent-model-not-product-authority")
observation = ObstacleObservation(
observation_id=f"{frame_id}:{geometry.owner_key}",
occupancy_key=f"{frame_id}:{geometry.owner_key}",
source_id=source_id,
frame_id=frame_id,
evidence_time_ns=evidence_time_ns,
basis=basis,
currentness=currentness,
occupied_support=occupied_support,
source_point_ids=source_point_ids,
metric_geometry=metric_geometry,
proposal_ids=proposals.get(geometry.owner_key, ()),
semantic_hint=geometry.semantic_label,
reason_codes=tuple(dict.fromkeys(reason_codes)),
)
if basis is EvidenceBasis.CAMERA and not observation.proposal_ids:
raise PerceptionAdapterError(
"camera-only geometry requires its source proposal binding"
)
observations.append(observation)
result = tuple(observations)
validate_exclusive_point_ownership(result)
return result
def temporal_obstacles_from_e34_projection(
projection: TemporalFrameProjection,
*,
coordinate_frame: str,
ttl_ns: int,
) -> tuple[TemporalObstacle, ...]:
"""Adapt current/held/expired E34 components without persistent identity claims."""
document = projection.document
results: list[TemporalObstacle] = []
for collection_name, state in (
("current", TemporalState.CURRENT),
("held", TemporalState.HELD),
("expired", TemporalState.EXPIRED),
):
collection = document.get(collection_name)
if not isinstance(collection, list):
raise PerceptionAdapterError(f"E34 projection {collection_name} must be an array")
for value in collection:
component = _object(value, f"E34 {collection_name} component")
temporal_id = _integer(component.get("temporal_id"), "E34 temporal id")
age_seconds = _number(
component.get("last_observed_age_seconds"),
"E34 component age",
)
age_ns = max(0, int(round(age_seconds * 1_000_000_000)))
history = _history(component.get("history_tail"))
cells = _cells(projection, component) if state is not TemporalState.EXPIRED else ()
centroid = (
None
if not cells
else _vector3(component.get("centroid_map_xyz_m"), "E34 centroid")
)
labels = _semantic_labels(component.get("semantic_provenance"))
results.append(
TemporalObstacle(
component_id=f"e34-component-{temporal_id}",
identity_scope="ephemeral",
state=state,
ttl_ns=ttl_ns,
last_hit_ns=max(0, history[-1].evidence_time_ns),
age_ns=age_ns,
association_basis=_safe_reason(component.get("association_reason")),
history=history,
cells=cells,
coordinate_frame=coordinate_frame if cells else None,
last_centroid_xyz_m=centroid,
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason="motion-not-estimated",
semantic_hint=labels[-1] if labels else None,
)
)
component_ids = [item.component_id for item in results]
if len(set(component_ids)) != len(component_ids):
raise PerceptionAdapterError("E34 projection contains duplicate temporal ids")
return tuple(results)
def _basis(
evidence_state: TrackGeometryEvidenceState,
owner_kind: TrackGeometryOwnerKind,
) -> EvidenceBasis:
if evidence_state is TrackGeometryEvidenceState.AGREE:
return EvidenceBasis.FUSED
if evidence_state is TrackGeometryEvidenceState.CONFLICT:
return EvidenceBasis.CONFLICT
if owner_kind is TrackGeometryOwnerKind.GEOMETRY_CLUSTER:
return EvidenceBasis.LIDAR
return EvidenceBasis.CAMERA
def _currentness(value: TrackGeometryCurrentness) -> EvidenceCurrentness:
if value is TrackGeometryCurrentness.CURRENT:
return EvidenceCurrentness.CURRENT
if value is TrackGeometryCurrentness.HELD:
return EvidenceCurrentness.HELD
return EvidenceCurrentness.STALE
def _history(value: object) -> tuple[HistorySample, ...]:
if not isinstance(value, list) or not value:
raise PerceptionAdapterError("E34 component history must be nonempty")
history: list[HistorySample] = []
for item in value[-32:]:
document = _object(item, "E34 history sample")
frame_index = _integer(document.get("frame_index"), "E34 history frame")
session_seconds = _number(document.get("session_seconds"), "E34 history time")
history.append(
HistorySample(
frame_id=f"e34-frame-{frame_index}",
evidence_time_ns=max(0, int(round(session_seconds * 1_000_000_000))),
centroid_xyz_m=_vector3(
document.get("centroid_map_xyz_m"),
"E34 history centroid",
),
)
)
return tuple(history)
def _cells(
projection: TemporalFrameProjection,
component: dict[str, object],
) -> tuple[GridCell, ...]:
start = _integer(component.get("cell_row_start"), "E34 cell start")
count = _integer(component.get("cell_row_count"), "E34 cell count")
if start < 0 or count < 0 or start + count > int(projection.cell_rows.shape[0]):
raise PerceptionAdapterError("E34 component cell range is invalid")
rows = projection.cell_rows[start : start + count]
return tuple(GridCell(int(row[0]), int(row[1]), int(row[2])) for row in rows)
def _semantic_labels(value: object) -> tuple[str, ...]:
document = _object(value, "E34 semantic provenance")
labels = document.get("labels")
if not isinstance(labels, list):
raise PerceptionAdapterError("E34 semantic labels must be an array")
return tuple(_safe_reason(label) for label in labels)
def _safe_reason(value: object) -> str:
if not isinstance(value, str) or not value:
raise PerceptionAdapterError("E34 reason must be a nonempty string")
return value
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise PerceptionAdapterError(f"{label} must be an object")
return value
def _integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool):
raise PerceptionAdapterError(f"{label} must be an integer")
return value
def _number(value: object, label: str) -> float:
if (
not isinstance(value, (int, float))
or isinstance(value, bool)
or not math.isfinite(float(value))
):
raise PerceptionAdapterError(f"{label} must be finite")
return float(value)
def _vector3(value: object, label: str) -> tuple[float, float, float]:
if not isinstance(value, list) or len(value) != 3:
raise PerceptionAdapterError(f"{label} must contain three values")
return (_number(value[0], label), _number(value[1], label), _number(value[2], label))
+287
View File
@@ -0,0 +1,287 @@
"""Executable Milestone 4 baseline binding.
The profile points at immutable local evidence without copying large or sensitive
artifacts into Git. Verification is explicit: missing evidence is a failure, not
an invitation to silently select a different source or experiment result.
"""
from __future__ import annotations
import hashlib
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final
BASELINE_SCHEMA: Final = "missioncore.perception-m4-baseline/v1"
REUSE_INVENTORY_SCHEMA: Final = "missioncore.perception-reuse-inventory/v1"
BASELINE_PROFILE_ID: Final = "m4-ravnoves00-recorded-realtime/v1"
BASELINE_SOURCE_ID: Final = "RAVNOVES00"
BASELINE_SESSION_ID: Final = "20260720T065719Z_viewer_live"
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_EXPECTED_EVIDENCE_ROLES: Final = {
"source-fusion",
"track-geometry",
"source-paced-worker",
"temporal-occupied",
"degradation-recovery",
"raw-fisheye-detector-capacity",
}
_BASELINE_KEYS: Final = {
"schema_version",
"profile_id",
"source",
"calibration",
"detector",
"evidence",
"authority",
"non_goals",
"rollback",
}
_EVIDENCE_KEYS: Final = {
"role",
"relative_path",
"schema_version",
"result_id",
"identity_sha256",
"file_sha256",
}
class BaselineContractError(ValueError):
"""The recorded-realtime baseline is ambiguous, mutated or incomplete."""
@dataclass(frozen=True, slots=True)
class BaselineEvidence:
role: str
relative_path: str
schema_version: str
result_id: str
identity_sha256: str
file_sha256: str
@dataclass(frozen=True, slots=True)
class BaselineProfile:
path: Path
document: dict[str, object]
evidence: tuple[BaselineEvidence, ...]
@dataclass(frozen=True, slots=True)
class BaselineVerification:
profile_id: str
source_id: str
session_id: str
verified_paths: tuple[str, ...]
def load_m4_baseline(path: Path) -> BaselineProfile:
"""Load and fail-closed validate the one admitted M4 baseline profile."""
document = _read_object(path)
_exact_keys(document, _BASELINE_KEYS, "baseline")
if document.get("schema_version") != BASELINE_SCHEMA:
raise BaselineContractError("baseline schema is incompatible")
if document.get("profile_id") != BASELINE_PROFILE_ID:
raise BaselineContractError("baseline profile identity changed")
source = _object(document.get("source"), "source")
if source.get("source_id") != BASELINE_SOURCE_ID:
raise BaselineContractError("M4 source must remain RAVNOVES00")
if source.get("session_id") != BASELINE_SESSION_ID:
raise BaselineContractError("M4 source session identity changed")
modalities = _string_array(source.get("modalities"), "source modalities")
if set(modalities) != {"image", "registered-point-increment", "pose"}:
raise BaselineContractError("baseline source must bind image, points and pose")
if _integer(source.get("frame_count"), "source frame count") != 4489:
raise BaselineContractError("baseline source frame count changed")
authority = _object(document.get("authority"), "authority")
if authority.get("mode") != "replay-simulated":
raise BaselineContractError("M4 authority must remain replay-simulated")
for key in (
"ground_truth",
"physical_live",
"physical_collision_accepted",
"commands_enabled",
"actuation_allowed",
"navigation_or_safety_accepted",
):
if authority.get(key) is not False:
raise BaselineContractError(f"baseline authority {key} must remain false")
evidence_items = document.get("evidence")
if not isinstance(evidence_items, list):
raise BaselineContractError("baseline evidence must be an array")
evidence = tuple(_evidence(item) for item in evidence_items)
roles = [item.role for item in evidence]
if len(set(roles)) != len(roles) or set(roles) != _EXPECTED_EVIDENCE_ROLES:
raise BaselineContractError("baseline evidence roles are incomplete or duplicated")
paths = [item.relative_path for item in evidence]
if len(set(paths)) != len(paths):
raise BaselineContractError("baseline evidence paths must be unique")
rollback = _object(document.get("rollback"), "rollback")
if rollback.get("worker_id") != "worker-006":
raise BaselineContractError("rollback worker identity changed")
if rollback.get("worker_node") != "DESKTOP-OPJ8J04":
raise BaselineContractError("rollback worker node changed")
entrypoint = rollback.get("entrypoint")
if not isinstance(entrypoint, str) or "run_e15_shadow_inference.py serve" not in entrypoint:
raise BaselineContractError("rollback E15 process identity is missing")
_digest(rollback.get("runner_sha256"), "rollback runner digest")
_digest(rollback.get("orchestrator_sha256"), "rollback orchestrator digest")
return BaselineProfile(path=path, document=document, evidence=evidence)
def verify_m4_baseline(repository_root: Path, profile: BaselineProfile) -> BaselineVerification:
"""Resolve every immutable evidence document and verify its exact digest."""
root = repository_root.resolve()
verified: list[str] = []
for item in profile.evidence:
evidence_path = (root / item.relative_path).resolve()
if root not in evidence_path.parents:
raise BaselineContractError("baseline evidence escapes the repository root")
if not evidence_path.is_file():
raise BaselineContractError(f"baseline evidence is missing: {item.relative_path}")
if _file_sha256(evidence_path) != item.file_sha256:
raise BaselineContractError(f"baseline evidence digest changed: {item.role}")
evidence_document = _read_object(evidence_path)
if evidence_document.get("schema_version") != item.schema_version:
raise BaselineContractError(f"baseline evidence schema changed: {item.role}")
if evidence_document.get("identity_sha256") != item.identity_sha256:
raise BaselineContractError(f"baseline evidence identity changed: {item.role}")
result_id = evidence_document.get("result_id")
if result_id is None and item.role == "source-fusion":
result_id = evidence_path.parent.name
if result_id != item.result_id:
raise BaselineContractError(f"baseline evidence result changed: {item.role}")
verified.append(item.relative_path)
source = _object(profile.document.get("source"), "source")
return BaselineVerification(
profile_id=BASELINE_PROFILE_ID,
source_id=_string(source.get("source_id"), "source id"),
session_id=_string(source.get("session_id"), "session id"),
verified_paths=tuple(verified),
)
def validate_reuse_inventory(path: Path) -> dict[str, object]:
"""Validate the M4 primitive/wrapper split used by architecture tests."""
document = _read_object(path)
_exact_keys(
document,
{
"schema_version",
"profile_id",
"reusable_primitives",
"historical_wrappers",
"rules",
},
"reuse inventory",
)
if document.get("schema_version") != REUSE_INVENTORY_SCHEMA:
raise BaselineContractError("reuse inventory schema is incompatible")
reusable = document.get("reusable_primitives")
wrappers = document.get("historical_wrappers")
if not isinstance(reusable, list) or not reusable:
raise BaselineContractError("reuse inventory has no admitted primitives")
if not isinstance(wrappers, list) or not wrappers:
raise BaselineContractError("reuse inventory has no historical wrappers")
reusable_modules = {_module(item, "reusable primitive") for item in reusable}
wrapper_modules = {_module(item, "historical wrapper") for item in wrappers}
if reusable_modules & wrapper_modules:
raise BaselineContractError("a module cannot be reusable and historical")
rules = _object(document.get("rules"), "reuse rules")
expected_rules = {
"historical_wrappers_are_product_dependencies": False,
"contracts_may_import_compute": False,
"graph_may_import_experiment_modules": False,
"providers_may_import_admitted_primitives": True,
"bulk_legacy_migration_required": False,
}
if rules != expected_rules:
raise BaselineContractError("reuse dependency rules changed")
return document
def _evidence(value: object) -> BaselineEvidence:
document = _object(value, "evidence item")
_exact_keys(document, _EVIDENCE_KEYS, "evidence item")
relative_path = _string(document.get("relative_path"), "evidence path")
path = Path(relative_path)
if path.is_absolute() or ".." in path.parts or path.suffix != ".json":
raise BaselineContractError("evidence path must be a relative JSON path")
return BaselineEvidence(
role=_string(document.get("role"), "evidence role"),
relative_path=relative_path,
schema_version=_string(document.get("schema_version"), "evidence schema"),
result_id=_string(document.get("result_id"), "evidence result id"),
identity_sha256=_digest(document.get("identity_sha256"), "evidence identity"),
file_sha256=_digest(document.get("file_sha256"), "evidence file digest"),
)
def _module(value: object, label: str) -> str:
document = _object(value, label)
return _string(document.get("module"), f"{label} module")
def _read_object(path: Path) -> dict[str, object]:
try:
value = json.loads(path.read_text("utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise BaselineContractError(f"cannot read baseline document: {path}") from exc
return _object(value, str(path))
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise BaselineContractError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
if set(document) != expected:
raise BaselineContractError(f"{label} fields are incompatible")
def _string(value: object, label: str) -> str:
if not isinstance(value, str) or not value:
raise BaselineContractError(f"{label} must be a nonempty string")
return value
def _integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool):
raise BaselineContractError(f"{label} must be an integer")
return value
def _string_array(value: object, label: str) -> tuple[str, ...]:
if not isinstance(value, list):
raise BaselineContractError(f"{label} must be an array")
return tuple(_string(item, label) for item in value)
def _digest(value: object, label: str) -> str:
digest = _string(value, label)
if _SHA256.fullmatch(digest) is None:
raise BaselineContractError(f"{label} must be a SHA-256 digest")
return digest
File diff suppressed because it is too large Load Diff
+334
View File
@@ -0,0 +1,334 @@
"""Provider protocols and one versioned graph configuration contract."""
from __future__ import annotations
import re
from collections.abc import Iterator
from dataclasses import dataclass
from enum import StrEnum
from typing import Final, Protocol
from .contracts import (
LocalObstacleMap,
ObjectProposal2D,
ObstacleObservation,
SourceEnvelope,
TemporalObstacle,
ThreatAssessment,
)
REFERENCE_GRAPH_CONFIG_SCHEMA: Final = "missioncore.reference-perception-graph-config/v1"
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$")
class ProviderContractError(ValueError):
"""A provider pin or graph execution policy is ambiguous or unsafe."""
class ProviderRole(StrEnum):
SOURCE = "source"
DETECTOR = "detector"
GEOMETRY = "geometry"
TEMPORAL = "temporal"
MOTION = "motion"
THREAT = "threat"
@dataclass(frozen=True, slots=True)
class ProviderPin:
role: ProviderRole
provider_id: str
version: str
revision: str
sha256: str
def __post_init__(self) -> None:
for value, label in (
(self.provider_id, "provider id"),
(self.version, "provider version"),
(self.revision, "provider revision"),
):
_identifier(value, label)
_digest(self.sha256, "provider digest")
def to_dict(self) -> dict[str, object]:
return {
"role": self.role.value,
"provider_id": self.provider_id,
"version": self.version,
"revision": self.revision,
"sha256": self.sha256,
}
@classmethod
def from_dict(cls, value: object) -> ProviderPin:
document = _object(value, "provider pin")
_exact_keys(
document,
{"role", "provider_id", "version", "revision", "sha256"},
"provider pin",
)
return cls(
role=_enum(ProviderRole, document.get("role"), "provider role"),
provider_id=_string(document, "provider_id"),
version=_string(document, "version"),
revision=_string(document, "revision"),
sha256=_string(document, "sha256"),
)
@dataclass(frozen=True, slots=True)
class QueuePolicy:
stage_id: str
capacity: int
deadline_ns: int
terminal_timeout_ns: int
def __post_init__(self) -> None:
_identifier(self.stage_id, "queue stage id")
_positive_integer(self.capacity, "queue capacity")
_positive_integer(self.deadline_ns, "queue deadline")
_positive_integer(self.terminal_timeout_ns, "terminal timeout")
if self.terminal_timeout_ns < self.deadline_ns:
raise ProviderContractError("terminal timeout cannot precede stage deadline")
def to_dict(self) -> dict[str, object]:
return {
"stage_id": self.stage_id,
"capacity": self.capacity,
"deadline_ns": self.deadline_ns,
"terminal_timeout_ns": self.terminal_timeout_ns,
}
@classmethod
def from_dict(cls, value: object) -> QueuePolicy:
document = _object(value, "queue policy")
_exact_keys(
document,
{"stage_id", "capacity", "deadline_ns", "terminal_timeout_ns"},
"queue policy",
)
return cls(
stage_id=_string(document, "stage_id"),
capacity=_integer(document, "capacity"),
deadline_ns=_integer(document, "deadline_ns"),
terminal_timeout_ns=_integer(document, "terminal_timeout_ns"),
)
@dataclass(frozen=True, slots=True)
class GraphAuthority:
mode: str = "replay-simulated"
physical_live: bool = False
commands_enabled: bool = False
actuation_allowed: bool = False
navigation_or_safety_accepted: bool = False
def __post_init__(self) -> None:
if self.mode != "replay-simulated":
raise ProviderContractError("M4 graph authority must be replay-simulated")
if any(
(
self.physical_live,
self.commands_enabled,
self.actuation_allowed,
self.navigation_or_safety_accepted,
)
):
raise ProviderContractError("M4 graph cannot publish physical or command authority")
def to_dict(self) -> dict[str, object]:
return {
"mode": self.mode,
"physical_live": self.physical_live,
"commands_enabled": self.commands_enabled,
"actuation_allowed": self.actuation_allowed,
"navigation_or_safety_accepted": self.navigation_or_safety_accepted,
}
@classmethod
def from_dict(cls, value: object) -> GraphAuthority:
document = _object(value, "graph authority")
_exact_keys(
document,
{
"mode",
"physical_live",
"commands_enabled",
"actuation_allowed",
"navigation_or_safety_accepted",
},
"graph authority",
)
return cls(
mode=_string(document, "mode"),
physical_live=_boolean(document, "physical_live"),
commands_enabled=_boolean(document, "commands_enabled"),
actuation_allowed=_boolean(document, "actuation_allowed"),
navigation_or_safety_accepted=_boolean(
document,
"navigation_or_safety_accepted",
),
)
@dataclass(frozen=True, slots=True)
class ReferencePerceptionGraphConfig:
graph_id: str
source_profile_id: str
providers: tuple[ProviderPin, ...]
queues: tuple[QueuePolicy, ...]
authority: GraphAuthority = GraphAuthority()
def __post_init__(self) -> None:
_identifier(self.graph_id, "graph id")
_identifier(self.source_profile_id, "source profile id")
roles = [provider.role for provider in self.providers]
if len(set(roles)) != len(roles) or set(roles) != set(ProviderRole):
raise ProviderContractError("graph must pin each provider role exactly once")
stage_ids = [queue.stage_id for queue in self.queues]
if not stage_ids or len(set(stage_ids)) != len(stage_ids):
raise ProviderContractError("graph queue policies must be nonempty and unique")
def to_dict(self) -> dict[str, object]:
return {
"schema_version": REFERENCE_GRAPH_CONFIG_SCHEMA,
"graph_id": self.graph_id,
"source_profile_id": self.source_profile_id,
"providers": [provider.to_dict() for provider in self.providers],
"queues": [queue.to_dict() for queue in self.queues],
"authority": self.authority.to_dict(),
}
@classmethod
def from_dict(cls, value: object) -> ReferencePerceptionGraphConfig:
document = _object(value, "reference graph config")
_exact_keys(
document,
{"schema_version", "graph_id", "source_profile_id", "providers", "queues", "authority"},
"reference graph config",
)
if document.get("schema_version") != REFERENCE_GRAPH_CONFIG_SCHEMA:
raise ProviderContractError("reference graph config schema is incompatible")
return cls(
graph_id=_string(document, "graph_id"),
source_profile_id=_string(document, "source_profile_id"),
providers=tuple(ProviderPin.from_dict(item) for item in _array(document, "providers")),
queues=tuple(QueuePolicy.from_dict(item) for item in _array(document, "queues")),
authority=GraphAuthority.from_dict(document.get("authority")),
)
class SourceProvider(Protocol):
provider_id: str
def envelopes(self) -> Iterator[SourceEnvelope]: ...
class DetectorProvider(Protocol):
provider_id: str
def detect(self, envelope: SourceEnvelope) -> tuple[ObjectProposal2D, ...]: ...
class GeometryAssociationProvider(Protocol):
provider_id: str
def associate(
self,
envelope: SourceEnvelope,
proposals: tuple[ObjectProposal2D, ...],
) -> tuple[ObstacleObservation, ...]: ...
class TemporalStateProvider(Protocol):
provider_id: str
def update(
self,
envelope: SourceEnvelope,
observations: tuple[ObstacleObservation, ...],
) -> tuple[TemporalObstacle, ...]: ...
class MotionProvider(Protocol):
provider_id: str
def estimate(
self,
envelope: SourceEnvelope,
obstacles: tuple[TemporalObstacle, ...],
) -> tuple[TemporalObstacle, ...]: ...
class ThreatProvider(Protocol):
provider_id: str
def assess(self, obstacle_map: LocalObstacleMap) -> tuple[ThreatAssessment, ...]: ...
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise ProviderContractError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
if set(document) != expected:
raise ProviderContractError(f"{label} fields are incompatible")
def _array(document: dict[str, object], key: str) -> list[object]:
value = document.get(key)
if not isinstance(value, list):
raise ProviderContractError(f"{key} must be an array")
return value
def _string(document: dict[str, object], key: str) -> str:
value = document.get(key)
if not isinstance(value, str) or not value:
raise ProviderContractError(f"{key} must be a nonempty string")
return value
def _boolean(document: dict[str, object], key: str) -> bool:
value = document.get(key)
if not isinstance(value, bool):
raise ProviderContractError(f"{key} must be boolean")
return value
def _integer(document: dict[str, object], key: str) -> int:
value = document.get(key)
if not isinstance(value, int) or isinstance(value, bool):
raise ProviderContractError(f"{key} must be an integer")
return value
def _positive_integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
raise ProviderContractError(f"{label} must be positive")
return value
def _identifier(value: str, label: str) -> str:
if _IDENTIFIER.fullmatch(value) is None:
raise ProviderContractError(f"{label} is not a safe identifier")
return value
def _digest(value: str, label: str) -> str:
if _SHA256.fullmatch(value) is None:
raise ProviderContractError(f"{label} must be a SHA-256 digest")
return value
def _enum(enum_type: type[ProviderRole], value: object, label: str) -> ProviderRole:
if not isinstance(value, str):
raise ProviderContractError(f"{label} must be a string")
try:
return enum_type(value)
except ValueError as exc:
raise ProviderContractError(f"{label} is incompatible") from exc