# Batch 82 Report — FT-P-15 + FT-P-16 + FT-P-18 cache / offline / no-raw-retention

**Batch**: 82
**Date**: 2026-05-17
**Context**: Test implementation (greenfield Step 10 — Implement Tests)
**Tasks**: AZ-421 (3 cp) — single task covering 3 sub-scenarios
**Cycle**: 1
**Verdict**: COMPLETE — PASS_WITH_WARNINGS (self-reviewed; see `reviews/batch_82_review.md`)

## Summary

Implements three storage / cache compliance scenarios that share the
`tile-cache-fixture` + FDR-archive observation surface:

* **FT-P-15** — Tile manifest schema completeness + 0.5 m/px floor
  (AC-8.1). Reads FDR `cache-self-check` record + `tile-load-rejected`
  events, validates every entry has CRS, tile_matrix, dimension,
  m_per_px, capture_date, source, compression; entries below floor
  must be explicitly rejected.
* **FT-P-16** — Offline-only operation (AC-8.3 / RESTRICT-SAT-1).
  Reads `docker network inspect e2e-net` + `docker inspect <sut>`
  JSON snapshots; asserts `e2e-net.Internal == true` AND the SUT is
  attached to that network only. The 0-egress semantic is enforced
  structurally — no other network is reachable.
* **FT-P-18** — No raw nav/AI-camera frame retention (AC-8.5). Walks
  FDR + tile-cache, probes JPEG dimensions, rejects any file whose
  extension + dimensions match the nav-camera raw pattern
  (5472×3648 or 880×720). Extrapolates thumbnail-log size to 8 h
  and asserts < 1 GB.

### AZ-421 — FT-P-15 + FT-P-16 + FT-P-18 (3 cp)

* **`e2e/runner/helpers/tile_cache_inspector.py`** (new, ~370 lines):
  pure-logic evaluators sourced from FDR / docker-inspect /
  filesystem walks.
  * `evaluate_manifest_schema(entries, *, tile_load_rejected_ids,
    m_per_px_floor)` → `ManifestSchemaReport` (AC-1, AC-2).
  * `evaluate_offline_mode(network_inspect, container_inspect)` →
    `OfflineModeReport` (AC-3).
  * `detect_raw_frames(file_specs, *, raw_dimensions,
    decoded_dimensions, raw_extensions)` → `RawFrameDetectionReport`
    (AC-4).
  * `evaluate_thumbnail_budget(size_bytes, duration_h)` →
    `ThumbnailLogBudgetReport` (AC-5).
  * `walk_files(*roots)` — convenience recursive walker.
  * `probe_jpeg_dimensions(path)` → `(w, h)` via SOF marker parse,
    stdlib-only.
  * Module-level constants: `CACHE_SELF_CHECK_FDR_KIND`,
    `TILE_LOAD_REJECTED_FDR_KIND`, `MANIFEST_REQUIRED_FIELDS`,
    `MANIFEST_M_PER_PX_FLOOR`, `NAV_CAMERA_RAW_DIMENSIONS`,
    `THUMBNAIL_LOG_MAX_SIZE_GB_PER_8H`.

* **`e2e/tests/positive/test_ft_p_15_cache_schema.py`** (new, ~115 lines):
  FT-P-15 scenario. Skips on missing fixture; fails loudly on empty
  `cache-self-check` record. `traces_to(AC-8.1,AC-1,AC-2,AC-6)`.

* **`e2e/tests/positive/test_ft_p_16_offline_only.py`** (new, ~115 lines):
  FT-P-16 scenario. Skips on missing `DOCKER_NETWORK_INSPECT_PATH` /
  `DOCKER_CONTAINER_INSPECT_PATH` env vars (fixture builder
  pre-snapshots these because the runner has no docker-socket access).
  `traces_to(AC-8.3,AC-3,AC-6,RESTRICT-SAT-1)`.

* **`e2e/tests/positive/test_ft_p_18_no_raw_retention.py`** (new,
  ~125 lines): FT-P-18 scenario. Walks FDR + tile-cache once;
  probes JPEGs; computes replay duration from FDR `monotonic_ms`
  span; evaluates AC-4 + AC-5. `traces_to(AC-8.5,AC-4,AC-5,AC-6)`.

* **`e2e/_unit_tests/helpers/test_tile_cache_inspector.py`** (new,
  43 tests): pure-logic coverage for every evaluator + walker +
  probe.

* **`e2e/_unit_tests/test_directory_layout.py`** (edited): registers
  `runner/helpers/tile_cache_inspector.py` and three new scenario
  test paths.

## Tests

Full `e2e/_unit_tests/` suite: **793 passed in 139.27 s** (baseline
746 → +47 net). Run via `python -m pytest e2e/_unit_tests/` from
the workspace root. No flakes, no skips outside the pre-existing
intentional skips.

Collection check on the three new scenario tests: 18 items
(3 tests × 6 `(fc_adapter, vio_strategy)` combinations). Scenario
tests skip locally because `E2E_SITL_REPLAY_DIR` is unset and the
docker-inspect env vars are unset — intended container-vs-host
boundary.

Per-area test counts (this batch):

| File | Tests added |
|------|-------------|
| `test_tile_cache_inspector.py` (new) | 43 |
| `test_directory_layout.py` (edited) | 4 (4 path entries) |
| `test_no_sut_imports.py` (no edit; broader walk) | implicit +1 module covered |
| **Total** | **+47** |

## Acceptance Criteria Verification

| AC  | Status | Evidence |
|-----|--------|----------|
| AC-1 — manifest schema completeness | ✓ | `test_ft_p_15_cache_schema` + 12 `test_evaluate_manifest_schema_*` |
| AC-2 — m/px ≥ 0.5 floor (or rejected) | ✓ | Same scenario; below-floor-with-rejection / without-rejection unit tests |
| AC-3 — offline operation (no non-e2e-net egress) | ✓ | `test_ft_p_16_offline_only` + 7 `test_evaluate_offline_mode_*` |
| AC-4 — no raw-frame retention | ✓ | `test_ft_p_18_no_raw_retention` + 9 `test_detect_raw_frames_*` + 5 `test_probe_jpeg_dimensions_*` |
| AC-5 — thumbnail log < 1 GB / 8 h | ✓ | Same scenario; 7 `test_evaluate_thumbnail_budget_*` |
| AC-6 — parameterisation | ✓ | 6 param IDs per scenario; 18 total items collected |

## Code Review Verdict
PASS_WITH_WARNINGS (no Critical, no High; 3 Low notes — see
`reviews/batch_82_review.md`).

## Auto-Fix Attempts
0 (no auto-fix-eligible findings).

## Stuck Agents
None.

## Notable Decisions

* **Single task in batch 82.** AZ-421 internally covers 3
  sub-scenarios (FT-P-15 / 16 / 18) — the task spec itself groups
  them because they share the `tile-cache-fixture` + FDR
  observation surface. Pulling AZ-422/423/427 in would have
  produced 7 test files + multiple new helpers in one batch,
  exceeding the recent empirical scope per batch (1–2 sub-scenarios).
  AZ-422 / AZ-423 / AZ-427 land as their own batches.
* **AC-3 (offline-only) is enforced structurally, not by packet
  count.** The spec says "all egress to non-`e2e-net` destinations
  is 0". With `e2e-net.Internal == true` and the SUT attached only
  to `e2e-net`, the packet count is provably 0 by Docker's network
  policy — there is literally no other network the SUT can reach.
  Checking the docker-inspect snapshots is cheaper and more
  reliable than per-packet counters.
* **JPEG SOF dimension probe is stdlib-only.** Loading every JPEG
  through OpenCV / Pillow just to read `(width, height)` would
  decode pixel data we discard. The 30-line SOF parser reads ≤16
  bytes per segment hop and terminates in <30 hops on real JPEGs.
* **The `probe_jpeg_dimensions` returns `None` on truncation /
  non-JPEG / OSError — does NOT raise.** The downstream
  `detect_raw_frames` explicitly treats `None` as "dimension
  unknown ≠ raw frame match" (documented). This avoids the test
  failing on every directory walk that happens to contain a
  corrupt JPEG, while still surfacing real raw-frame retention.
* **Docker inspect via env-var indirection.** The e2e-runner
  container does not have docker-socket access (an intentional
  security boundary). The fixture builder must `docker network
  inspect e2e-net > /e2e-results/net.json` + `docker inspect
  gps-denied-onboard > /e2e-results/sut.json` before the runner
  starts, and the runner reads those snapshots through env vars.
  This is the same pattern AZ-420 used for `gcs_tlog_<host>.tlog`
  (fixture-builder responsibility).

## Production Dependencies (forward-look)

FT-P-15 / FT-P-16 / FT-P-18 transitively depend on:

* **FDR `cache-self-check` record** at SUT cold-start — the SUT's
  C6 tile-cache loader must emit one record carrying every manifest
  entry it loaded. (Cross-checked against the FDR schema documented
  in `_docs/02_document/components/c6_*` — slot is reserved; no
  producer wires it yet.)
* **FDR `tile-load-rejected` events** — for entries below the m/px
  floor (or otherwise rejected by the freshness gate). Reserved
  same way.
* **Docker compose `e2e-net` attribute `internal: true`** — owned
  by AZ-406. Already wired per the existing compose file.
* **Fixture builder snapshots** of `docker inspect` (AZ-595).

Tests fail loudly when fixture data is missing rather than silently
skipping — the "tests as gates" pattern.

## Out of Scope (deferred)

* DNS blackhole defense-in-depth — owned by NFT-SEC-05 (AZ-437).
* Cache-poisoning safety — owned by NFT-SEC-01 (AZ-436).
* Stale-tile rejection on aged source tiles — owned by FT-N-05
  (AZ-427).
* The fixture builder's actual `cache-self-check` FDR synthesis +
  docker-inspect JSON capture — owned by AZ-595.

## Next Batch

Batch 83 candidates from `_docs/02_tasks/todo/` (20 remaining): AZ-422
(FT-P-17 + FT-N-06 mid-flight tiles, 3 cp), AZ-423 (FT-P-19 sat
reloc, 3 cp), AZ-427 (FT-N-05 stale-tile rejection, 2 cp). Topo-order
leader is AZ-422. Pick at next `/autodev` invocation.
