Files
NODEDC_MISSION_CORE/src/k1link/perception/graph_contracts.py
T

152 lines
4.5 KiB
Python

"""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",
]