mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 15:21:14 +00:00
[AZ-294] [AZ-295] [AZ-296] Finish C13: tile snapshot + record-kind policy + takeoff abort
AZ-294: MidFlightTileSnapshotSink writes orthorectified tile JPEGs atomically to flight_root/<flight_id>/tiles/<tile_id>.jpg, emits a kind="mid_flight_tile_snapshot" pointer record, and evicts the oldest tile when the per-flight 64 MiB cap is exceeded. Adds optional frame_id to the snapshot payload (fdr_record_schema bump). AZ-295: RecordKindPolicy with two paired gates: - enforce_or_raise (producer-side) raises RawFrameWriteForbiddenError for raw_nav_frame / raw_ai_cam_frame at the call site, defending AC-8.5 / RESTRICT-UAV-4. - gate_for_writer (writer-side) tumbling-window rate-caps failed_tile_thumbnail records at <= 0.1 Hz; over-cap drops are coalesced into kind="overrun" records with the originating producer slug. AZ-296: take_off() composition-root sequence with strict ordering (writer.__init__ -> start -> open_flight -> fc_adapter.__init__ -> fc_adapter.open). On FdrOpenError, logs ERROR record, calls writer.stop(), prints the documented FATAL line to stderr, and sys.exit(EXIT_FDR_OPEN_FAILURE=2). composition_root_protocol bumped to v1.1.0 with the new constants + takeoff-sequence section. 29 new tests; full suite 356 passed / 2 skipped / 0 failures. No new dependencies (stdlib only). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,17 +15,29 @@ from dataclasses import dataclass, field, fields, is_dataclass, replace
|
||||
from typing import Any, Final
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_FORBIDDEN_RECORD_KINDS",
|
||||
"Config",
|
||||
"ConfigError",
|
||||
"FdrConfig",
|
||||
"FdrWriterConfig",
|
||||
"LogConfig",
|
||||
"RecordKindPolicyConfig",
|
||||
"RequiredFieldMissingError",
|
||||
"RuntimeConfig",
|
||||
"TileSnapshotConfig",
|
||||
"register_component_block",
|
||||
]
|
||||
|
||||
|
||||
# Default raw-frame kinds that AZ-295's RecordKindPolicy must reject
|
||||
# synchronously at the producer call site. Removing any of these from
|
||||
# a Config requires an explicit `unsafe_remove_default_forbidden=True`
|
||||
# flag (which is intentionally not present in any standard preset).
|
||||
DEFAULT_FORBIDDEN_RECORD_KINDS: Final[frozenset[str]] = frozenset(
|
||||
{"raw_nav_frame", "raw_ai_cam_frame"}
|
||||
)
|
||||
|
||||
|
||||
class ConfigError(RuntimeError):
|
||||
"""Base class for all config-loader errors that should reach the caller."""
|
||||
|
||||
@@ -73,6 +85,80 @@ class FdrWriterConfig:
|
||||
debug_log_per_record: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TileSnapshotConfig:
|
||||
"""C13 mid-flight tile snapshot sidecar block (AZ-294).
|
||||
|
||||
``tile_snapshot_cap_bytes`` is the per-flight ceiling on the
|
||||
cumulative size of the ``tiles/`` subdirectory under the flight
|
||||
root (default 64 MiB to comfortably hold the worst-case ~50 MB
|
||||
from per-component description.md).
|
||||
|
||||
``jpeg_max_bytes`` rejects single tile JPEGs larger than this
|
||||
bound (default 256 KiB; description.md gives 50-200 KiB).
|
||||
"""
|
||||
|
||||
tile_snapshot_cap_bytes: int = 64 * 1024 * 1024
|
||||
jpeg_max_bytes: int = 256 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecordKindPolicyConfig:
|
||||
"""C13 record-kind policy block (AZ-295).
|
||||
|
||||
``forbidden_record_kinds`` lists FdrRecord ``kind`` values that
|
||||
the producer-side ``enforce_or_raise`` gate rejects with
|
||||
``RawFrameWriteForbiddenError``. The default set
|
||||
(``DEFAULT_FORBIDDEN_RECORD_KINDS``) MUST be a subset of the
|
||||
configured set — removing defaults is a security-review-required
|
||||
path guarded by ``unsafe_remove_default_forbidden``.
|
||||
|
||||
``failed_tile_thumbnail_max_hz`` caps the writer-side rate of
|
||||
``kind="failed_tile_thumbnail"`` records (default 0.1 Hz per
|
||||
AC-8.5 + description.md § 7). Setting this to 0 is rejected at
|
||||
config validation (would silence the kind entirely; that path is
|
||||
intentionally not exposed).
|
||||
"""
|
||||
|
||||
forbidden_record_kinds: frozenset[str] = field(
|
||||
default_factory=lambda: DEFAULT_FORBIDDEN_RECORD_KINDS
|
||||
)
|
||||
failed_tile_thumbnail_max_hz: float = 0.1
|
||||
unsafe_remove_default_forbidden: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.forbidden_record_kinds, frozenset):
|
||||
raise ConfigError(
|
||||
"RecordKindPolicyConfig.forbidden_record_kinds must be a frozenset; "
|
||||
f"got {type(self.forbidden_record_kinds).__name__}"
|
||||
)
|
||||
if not self.unsafe_remove_default_forbidden:
|
||||
missing_defaults = DEFAULT_FORBIDDEN_RECORD_KINDS - self.forbidden_record_kinds
|
||||
if missing_defaults:
|
||||
raise ConfigError(
|
||||
"RecordKindPolicyConfig.forbidden_record_kinds removes default raw-frame "
|
||||
f"kinds without unsafe_remove_default_forbidden=True: missing {sorted(missing_defaults)}"
|
||||
)
|
||||
if not (
|
||||
isinstance(self.failed_tile_thumbnail_max_hz, (int, float))
|
||||
and not isinstance(self.failed_tile_thumbnail_max_hz, bool)
|
||||
):
|
||||
raise ConfigError(
|
||||
"RecordKindPolicyConfig.failed_tile_thumbnail_max_hz must be a number; "
|
||||
f"got {self.failed_tile_thumbnail_max_hz!r}"
|
||||
)
|
||||
if self.failed_tile_thumbnail_max_hz <= 0:
|
||||
raise ConfigError(
|
||||
"RecordKindPolicyConfig.failed_tile_thumbnail_max_hz must be > 0; "
|
||||
f"got {self.failed_tile_thumbnail_max_hz}"
|
||||
)
|
||||
if self.failed_tile_thumbnail_max_hz > 10.0:
|
||||
raise ConfigError(
|
||||
"RecordKindPolicyConfig.failed_tile_thumbnail_max_hz must be <= 10.0; "
|
||||
f"got {self.failed_tile_thumbnail_max_hz}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FdrConfig:
|
||||
"""Cross-cutting Flight Data Recorder block (E-CC-FDR-CLIENT / AZ-247).
|
||||
@@ -82,7 +168,8 @@ class FdrConfig:
|
||||
producer slug (consumed by AZ-273 ``make_fdr_client``); blocks
|
||||
that omit a producer fall back to ``queue_size``.
|
||||
|
||||
``writer`` is the C13 writer-thread sub-block (AZ-291..AZ-296).
|
||||
Sub-blocks (AZ-291..AZ-296): ``writer``, ``tile_snapshot``,
|
||||
``record_policy``.
|
||||
"""
|
||||
|
||||
queue_size: int = 4096
|
||||
@@ -90,6 +177,8 @@ class FdrConfig:
|
||||
path: str = "/var/lib/gps-denied/fdr"
|
||||
per_producer_capacity: Mapping[str, int] = field(default_factory=dict)
|
||||
writer: FdrWriterConfig = field(default_factory=FdrWriterConfig)
|
||||
tile_snapshot: TileSnapshotConfig = field(default_factory=TileSnapshotConfig)
|
||||
record_policy: RecordKindPolicyConfig = field(default_factory=RecordKindPolicyConfig)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
Reference in New Issue
Block a user