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

225 lines
7.0 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"
GRAPH_RESULT_SCHEMA_V2: Final = "missioncore.reference-perception-graph-result/v2"
TERMINAL_OUTCOME_SCHEMA: Final = "missioncore.perception-terminal-outcome/v1"
REFERENCE_GRAPH_ID: Final = "reference-perception-graph/v1"
REFERENCE_GRAPH_ID_V2: Final = "reference-perception-graph/v2"
_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 GraphRunMode(StrEnum):
"""Bounded queue behavior for realtime and deterministic replay."""
SOURCE_PACED_LATEST_WINS = "source-paced-latest-wins"
LOSSLESS_REPLAY = "lossless-replay"
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,
}
@dataclass(frozen=True, slots=True)
class GraphRunResultV2:
graph_id: str
source_profile_id: str
run_mode: GraphRunMode
state: GraphState
admitted_count: int
terminal_outcomes: tuple[TerminalOutcome, ...]
deliveries: tuple[DeliveredFrame, ...]
queue_high_watermarks: tuple[tuple[str, int], ...]
canonical_payload_sha256: str
def to_dict(self) -> dict[str, object]:
return {
"schema_version": GRAPH_RESULT_SCHEMA_V2,
"graph_id": self.graph_id,
"source_profile_id": self.source_profile_id,
"run_mode": self.run_mode.value,
"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],
"queue_high_watermarks": dict(self.queue_high_watermarks),
"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(),
)
def build_graph_run_result_v2(
*,
graph_id: str,
source_profile_id: str,
run_mode: GraphRunMode,
state: GraphState,
admitted_count: int,
outcomes: tuple[TerminalOutcome, ...],
deliveries: tuple[DeliveredFrame, ...],
queue_high_watermarks: tuple[tuple[str, int], ...],
) -> GraphRunResultV2:
canonical = {
"graph_id": graph_id,
"source_profile_id": source_profile_id,
"run_mode": run_mode.value,
"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 GraphRunResultV2(
graph_id=graph_id,
source_profile_id=source_profile_id,
run_mode=run_mode,
state=state,
admitted_count=admitted_count,
terminal_outcomes=outcomes,
deliveries=deliveries,
queue_high_watermarks=queue_high_watermarks,
canonical_payload_sha256=hashlib.sha256(payload).hexdigest(),
)
__all__ = [
"GRAPH_RESULT_SCHEMA",
"GRAPH_RESULT_SCHEMA_V2",
"REFERENCE_GRAPH_ID",
"REFERENCE_GRAPH_ID_V2",
"TERMINAL_OUTCOME_SCHEMA",
"DeliveredFrame",
"GraphExecutionError",
"GraphRunResult",
"GraphRunResultV2",
"GraphRunMode",
"GraphState",
"TerminalOutcome",
"TerminalOutcomeType",
"build_graph_run_result",
"build_graph_run_result_v2",
]