# Batch 45 — Cycle 1 Report **Date**: 2026-05-13 **Batch**: 45 **Tasks**: AZ-341 (C2 FAISS HNSW Retrieve Wiring, 3pt) **Status**: complete; ticket transitioned to "In Testing". ## Scope AZ-341 delivers `FaissBridge` — the cross-strategy shared plumbing that connects every concrete C2 `VprStrategy.retrieve_topk` to C6's FAISS HNSW `DescriptorIndex.search_topk` surface. The bridge owns the defended-in-depth INV-4 check (exactly K results, distance-ascending), the WARN-threshold check on `distances[0]`, the optional per-frame DEBUG `top10_distances` log, and the `vpr.retrieve_topk` FDR record emission with monotonic-ns latency measurement. Companion deliverables in the same batch: - `DescriptorIndexCut` Protocol — the AZ-507 consumer-side structural cut that keeps `c2_vpr/*.py` import-free of `c6_tile_cache.*`. - `C2VprConfig` gains two operator-tunable knobs: `warn_top1_threshold` (default 0.30 — task-spec Risk 3 conservative placeholder pending FT-P-19 telemetry) and `debug_per_frame_distances` (default OFF — Risk 2 mitigation against journald flood). - `fdr_client.records` registers `vpr.retrieve_topk` in `KNOWN_PAYLOAD_KEYS` and the AZ-272 roundtrip schema test gains a fixture for the new kind (mirroring the Batch-44 `c12.reloc.requested` pattern). Unblocks each future C2 strategy (AZ-337 UltraVPR, AZ-338 NetVLAD, AZ-339 MegaLoc+MixVPR, AZ-340 SelaVPR+EigenPlaces+SALAD): every strategy's `retrieve_topk` shrinks to a one-line delegation `return self._faiss_bridge.retrieve(query, k, backbone_label=...)`, eliminating the seven-way duplication risk the task spec called out. ## Architectural Decisions ### 1. The bridge is stricter than the c6 contract — by design C6's `descriptor_index.md` Invariant I-2 says `search_topk` "MAY return fewer than K results when the corpus has fewer than K vectors". The AZ-341 task spec asks the bridge to enforce `== K` and raise `IndexUnavailableError` on undersized returns. The two contracts are complementary, not contradictory: - C6's I-2 is a **structural** invariant (the return-shape upper bound, not a guarantee of full results). - AZ-341's INV-4 is an **operational** invariant (a corpus with fewer than K vectors is operationally degraded; the system can not function — downstream RANSAC matching needs the full top-K spread). The bridge's module docstring documents this on-purpose strictness. The defended-in-depth posture is the task-spec's explicit Risk-1 mitigation against silent C6 regressions. ### 2. The bridge propagates the c6 `IndexUnavailableError` unchanged AC-4 is explicit: "the bridge does not catch and re-raise". The bridge has no `try/except IndexUnavailableError`. When the c6 stale-handle / sidecar / dim-mismatch defence fires, the original c6 exception escapes directly to the strategy and onwards. This conflicts with the `c6_tile_cache.errors.IndexUnavailableError` module docstring's expectation that "the C2 family is the closed envelope a C5/C2.5 consumer sees; the C6 family is the storage-layer error a concrete strategy is responsible for rewrapping". The implementation follows the task-spec letter; surfacing the gap as Finding F2 of the per-batch review for spec/code-comment alignment. ### 3. `_iso_ts_from_clock` is duplicated — accepted for this batch The helper is six lines that appear inline in c2 / c5 / c11 / c12. Each component avoids importing the others' helpers (per AZ-507 shape). A future shared helper would live in `shared/helpers/` (L1) but factoring is deferred to AZ-508 (the hygiene-only PBI already in `todo/` covering ISO-timestamp consolidation across the suite). Captured as Finding F1. ### 4. The dropped `normaliser` constructor parameter The task spec's Outcome § Constructor lists `normaliser: DescriptorNormaliser` but the documented `retrieve` body never calls it — the `VprStrategy` Protocol INV-3 already requires `VprQuery.embedding` to arrive L2-normalised. The bridge omits the parameter (no behaviour change vs the documented body). Captured as Finding F2 for spec hygiene. ## Files Changed ### Production source (new) - `src/gps_denied_onboard/components/c2_vpr/_faiss_bridge.py` — `FaissBridge` class (~250 LOC), `_verify_invariants` / `_log_invariant_violation` / `_emit_fdr_record` / `_iso_ts_from_clock` helpers, module-level FDR / log `kind` constants. - `src/gps_denied_onboard/components/c2_vpr/descriptor_index_cut.py` — `DescriptorIndexCut` Protocol + `TileIdTuple` alias (~50 LOC). ### Production source (modified) - `src/gps_denied_onboard/components/c2_vpr/config.py` — `C2VprConfig` gains `warn_top1_threshold: float = 0.30` and `debug_per_frame_distances: bool = False` plus their validators. - `src/gps_denied_onboard/components/c2_vpr/__init__.py` — re-exports `DescriptorIndexCut` and `TileIdTuple` for the composition root's c6 adapter. - `src/gps_denied_onboard/fdr_client/records.py` — registers `vpr.retrieve_topk` in `KNOWN_PAYLOAD_KEYS` with payload field set `{frame_id, backbone_label, top10_distances, latency_us}`. ### Tests (new) - `tests/unit/c2_vpr/test_faiss_bridge.py` — 22 tests covering AC-1..AC-11, NFR-perf microbench (p95 ≤ 0.5 ms), constructor validation, retrieve-argument validation. Uses `_FakeDescriptorIndex`, `_StubClock`, and a real `FdrClient` with power-of-two capacity 8 to verify both the success enqueue pattern and the OVERRUN warn path. ### Tests (modified) - `tests/unit/test_az272_fdr_record_schema.py` — adds a `vpr.retrieve_topk` payload to the `_make_fdr_payload` fixture so the schema roundtrip + envelope tests cover the new kind. ### Docs / process - `_docs/03_implementation/reviews/batch_45_review.md` — per-batch code review (verdict PASS_WITH_WARNINGS, 2 Low findings). - `_docs/_autodev_state.md` — sub_step bumped through the batch loop. ## Test Results ``` 1565 passed, 80 skipped in 69.9s ``` Versus pre-batch (Batch 44 final): `1543 passed, 80 skipped`. Delta: +22 new tests (all in `tests/unit/c2_vpr/test_faiss_bridge.py`), zero regressions, zero new skips. ## Code Review Verdict: **PASS_WITH_WARNINGS** — see `_docs/03_implementation/reviews/batch_45_review.md`. Two Low-severity findings: | # | Severity | Category | Title | |---|----------|----------|-------| | F1 | Low | Maintainability | Duplicated `_iso_ts_from_clock` helper across c2/c5/c11/c12 — defer to AZ-508 | | F2 | Low | Spec-Gap | Task spec lists unused `normaliser` parameter — surface to user / spec hygiene | No Critical, no High, no Medium. The Auto-Fix Gate accepts the verdict without intervention. ## AC Coverage Summary All 11 acceptance criteria + NFR-perf have at least one covering test. Detailed AC ↔ test mapping in `_docs/03_implementation/reviews/batch_45_review.md` § Phase 2. ## Tracker - AZ-341 transitioned `To Do` → `In Progress` at batch start. - AZ-341 transitioned `In Progress` → `In Testing` after the commit landed. ## Cumulative Review (batches 43-45) Last cumulative review was for batches 40-42. With Batch 45 closed (43, 44, 45 since the last cumulative), the implement skill's Step 14.5 cumulative review fires next at K=3. ## Next Batch Several C-component slices are now unblocked: - C1: AZ-333 (VINS-Mono strategy, research), AZ-334 (KLT/RANSAC mandatory baseline). Then AZ-335 (warm-start) once 333+334 land. - C2: AZ-337 (UltraVPR primary, 5pt), AZ-338 (NetVLAD baseline, 3pt), AZ-339 (MegaLoc+MixVPR research), AZ-340 (SelaVPR+ EigenPlaces+SALAD research). - C3: AZ-345 / AZ-346 / AZ-347 — pending the C2.5 ReRanker tasks. A natural Batch-46 candidate would be a C1 mandatory simple-baseline (AZ-334) — single 5pt task, OpenCV-based, no model weights — but the next-batch selection will go through the implement skill's Step 3 (Compute Next Batch) afresh.