"""Immutable lifecycle and result documents for the reference perception graph.""" from __future__ import annotations import hashlib import json import re from dataclasses import dataclass from enum import StrEnum from typing import Final from .contracts import LocalObstacleMap, ThreatAssessment GRAPH_RESULT_SCHEMA: Final = "missioncore.reference-perception-graph-result/v1" TERMINAL_OUTCOME_SCHEMA: Final = "missioncore.perception-terminal-outcome/v1" REFERENCE_GRAPH_ID: Final = "reference-perception-graph/v1" _SAFE_IDENTIFIER: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$") class GraphExecutionError(RuntimeError): """The graph cannot preserve its bounded lifecycle or product accounting.""" class GraphState(StrEnum): CREATED = "created" STARTING = "starting" RUNNING = "running" STOPPING = "stopping" STOPPED = "stopped" CANCELLED = "cancelled" FAILED = "failed" class TerminalOutcomeType(StrEnum): DELIVERED = "delivered" SUPERSEDED = "superseded" STALE = "stale" REJECTED = "rejected" FAILED = "failed" UNAVAILABLE = "unavailable" @dataclass(frozen=True, slots=True) class TerminalOutcome: source_id: str session_id: str frame_id: str sequence: int outcome: TerminalOutcomeType stage_id: str reason: str def __post_init__(self) -> None: for value in (self.source_id, self.session_id, self.frame_id, self.stage_id, self.reason): if _SAFE_IDENTIFIER.fullmatch(value) is None: raise GraphExecutionError("terminal outcome contains an unsafe identifier") if self.sequence < 0: raise GraphExecutionError("terminal sequence must be nonnegative") def to_dict(self) -> dict[str, object]: return { "schema_version": TERMINAL_OUTCOME_SCHEMA, "source_id": self.source_id, "session_id": self.session_id, "frame_id": self.frame_id, "sequence": self.sequence, "outcome": self.outcome.value, "stage_id": self.stage_id, "reason": self.reason, } @dataclass(frozen=True, slots=True) class DeliveredFrame: sequence: int obstacle_map: LocalObstacleMap threats: tuple[ThreatAssessment, ...] def canonical_dict(self) -> dict[str, object]: obstacle_map = self.obstacle_map.to_dict() obstacle_map.pop("generated_monotonic_ns") obstacle_map.pop("output_age_ns") return { "sequence": self.sequence, "obstacle_map": obstacle_map, "threats": [item.to_dict() for item in self.threats], } @dataclass(frozen=True, slots=True) class GraphRunResult: graph_id: str source_profile_id: str state: GraphState admitted_count: int terminal_outcomes: tuple[TerminalOutcome, ...] deliveries: tuple[DeliveredFrame, ...] canonical_payload_sha256: str def to_dict(self) -> dict[str, object]: return { "schema_version": GRAPH_RESULT_SCHEMA, "graph_id": self.graph_id, "source_profile_id": self.source_profile_id, "state": self.state.value, "admitted_count": self.admitted_count, "terminal_outcomes": [item.to_dict() for item in self.terminal_outcomes], "deliveries": [item.canonical_dict() for item in self.deliveries], "canonical_payload_sha256": self.canonical_payload_sha256, } def build_graph_run_result( *, graph_id: str, source_profile_id: str, state: GraphState, admitted_count: int, outcomes: tuple[TerminalOutcome, ...], deliveries: tuple[DeliveredFrame, ...], ) -> GraphRunResult: canonical = { "graph_id": graph_id, "source_profile_id": source_profile_id, "terminal_outcomes": [item.to_dict() for item in outcomes], "deliveries": [item.canonical_dict() for item in deliveries], } payload = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode() return GraphRunResult( graph_id=graph_id, source_profile_id=source_profile_id, state=state, admitted_count=admitted_count, terminal_outcomes=outcomes, deliveries=deliveries, canonical_payload_sha256=hashlib.sha256(payload).hexdigest(), ) __all__ = [ "GRAPH_RESULT_SCHEMA", "REFERENCE_GRAPH_ID", "TERMINAL_OUTCOME_SCHEMA", "DeliveredFrame", "GraphExecutionError", "GraphRunResult", "GraphState", "TerminalOutcome", "TerminalOutcomeType", "build_graph_run_result", ]