[AZ-223] [AZ-224] [AZ-225] [AZ-227] Add runtime gateways

Implement the first runtime component boundaries around the shared
contracts so downstream batches can consume typed frame, MAVLink, tile,
and FDR behavior with focused tests and batch evidence.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-03 18:01:13 +03:00
parent aab11e488e
commit e86084da6b
23 changed files with 1106 additions and 13 deletions
+108
View File
@@ -2,6 +2,18 @@
from typing import Any, Protocol
from shared.contracts import FdrEvent
from shared.errors import ErrorEnvelope
from .types import (
FdrAppendResult,
FdrExportRequest,
FdrExportResult,
FdrHealth,
FdrPayload,
FdrSegmentSummary,
)
class FlightRecorder(Protocol):
"""Append-only event recorder for runtime evidence."""
@@ -11,3 +23,99 @@ class FlightRecorder(Protocol):
def export(self) -> Any:
"""Export recorded evidence for post-flight analysis."""
class InMemoryFlightRecorder:
"""Bounded append-only recorder for runtime evidence metadata."""
def __init__(self, segment_limit_bytes: int, storage_limit_bytes: int) -> None:
if segment_limit_bytes <= 0:
raise ValueError("segment_limit_bytes must be positive")
if storage_limit_bytes < segment_limit_bytes:
raise ValueError("storage_limit_bytes must be at least one segment")
self._segment_limit_bytes = segment_limit_bytes
self._storage_limit_bytes = storage_limit_bytes
self._segments: list[list[FdrEvent]] = [[]]
self._segment_bytes: list[int] = [0]
self._used_bytes = 0
@property
def health(self) -> FdrHealth:
if self._used_bytes >= self._storage_limit_bytes:
return FdrHealth(
status="critical",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage limit reached",
)
if self._used_bytes >= int(self._storage_limit_bytes * 0.9):
return FdrHealth(
status="degraded",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage nearing limit",
)
return FdrHealth(
status="ready",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage ready",
)
def append_event(self, event: FdrEvent, payload: FdrPayload) -> FdrAppendResult:
if self._used_bytes + payload.size_bytes > self._storage_limit_bytes:
return FdrAppendResult(
appended=False,
error=ErrorEnvelope(
component="fdr_observability",
category="resource",
message="fdr storage limit reached",
severity="critical",
retryable=False,
),
)
rollover = False
if self._segment_bytes[-1] + payload.size_bytes > self._segment_limit_bytes:
self._segments.append([])
self._segment_bytes.append(0)
rollover = True
segment_index = len(self._segments) - 1
stored_event = event.model_copy(update={"payload_ref": payload.ref})
self._segments[segment_index].append(stored_event)
self._segment_bytes[segment_index] += payload.size_bytes
self._used_bytes += payload.size_bytes
return FdrAppendResult(
appended=True,
event=stored_event,
segment_id=self._segment_id(segment_index),
rollover=rollover,
)
def export(self, request: FdrExportRequest) -> FdrExportResult:
segments = tuple(
FdrSegmentSummary(
segment_id=self._segment_id(index),
event_count=len(events),
bytes_used=self._segment_bytes[index],
)
for index, events in enumerate(self._segments)
if events
)
evidence_ref = f"fdr://exports/{request.mission_id}/{request.run_id}/evidence.json"
analytics_ref = (
f"fdr://exports/{request.mission_id}/{request.run_id}/analytics.parquet"
if request.include_analytics
else None
)
return FdrExportResult(
produced=True,
evidence_ref=evidence_ref,
segments=segments,
analytics_ref=analytics_ref,
)
def _segment_id(self, index: int) -> str:
return f"segment-{index + 1:04d}"