"""Post-run filesystem read of the FDR archive. The FDR archive is a line-delimited JSON record stream per AZ-272 / AZ-273. Each line is an `FdrRecord` envelope (producer_id, type, monotonic_ms, payload). The runner image must NEVER import the SUT's FdrRecord schema directly — it parses the JSON bytes and validates against a duplicate record-type allowlist baked into this module. Public surface only; concrete parser + assertion helpers are owned by AZ-441 (NFT-LIM-02 — FDR size budget) and the resilience scenario tasks that need to crawl the archive (AZ-432, AZ-433, AZ-435). """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Iterator @dataclass(frozen=True) class FdrRecord: """Mirror of `gps_denied_onboard.fdr_client.records.FdrRecord` — public-boundary copy. The schema is duplicated intentionally; if the SUT's FDR schema evolves in a breaking way, this duplicate file fails to parse (visible drift) rather than silently following along. """ producer_id: str monotonic_ms: int record_type: str payload: dict[str, object] def iter_records(fdr_archive_root: Path) -> Iterator[FdrRecord]: """Iterate every FDR record in the archive root (ordered by monotonic_ms). Raises NotImplementedError until AZ-441 supplies the orjson-backed parser. """ raise NotImplementedError( "fdr_reader.iter_records is owned by AZ-441 — AZ-406 supplies only " "the public surface." ) def archive_size_bytes(fdr_archive_root: Path) -> int: """Sum the size of every file under ``fdr_archive_root``. Concrete implementation here — it's a thin os.walk + stat loop that NFT-LIM-02 needs as soon as a real archive lands. """ if not fdr_archive_root.exists(): return 0 total = 0 for p in fdr_archive_root.rglob("*"): if p.is_file(): total += p.stat().st_size return total