Foundational scaffolding for every concrete C2 backbone (UltraVPR,
NetVLAD, MegaLoc, MixVPR, SelaVPR, EigenPlaces, SALAD — AZ-337..AZ-340)
and the C2.5 ReRanker consumer side. No backbone is implemented here.
* VprStrategy Protocol (embed_query / retrieve_topk / descriptor_dim)
+ BackbonePreprocessor C2-internal Protocol (NOT in Public API per
description.md § 6).
* DTOs in L1 _types/vpr.py — VprQuery, VprCandidate, VprResult; all
frozen + slots; tuple-not-list for VprResult.candidates so the
immutability invariant truly holds.
* Error family: VprError + VprBackboneError + VprPreprocessError +
IndexUnavailableError; same-named but namespace-distinct from
c6_tile_cache.IndexUnavailableError (the c2 family is the closed
envelope C5 / C2.5 consume; concrete strategies rewrap the C6 form).
* C2VprConfig (strategy enum + backbone_weights_path + faiss_index_path)
with strict validation at load; registered into Config.components on
c2_vpr import.
* build_vpr_strategy factory with 7-strategy resolution table, lazy
import, BUILD_VPR_<variant> gating, ImportError→
StrategyNotAvailableError mapping, and pre-flight descriptor_dim
match against DescriptorIndex.descriptor_dim() — mismatch fires
ConfigError at startup, NOT at first frame.
Contract change vs the v1.0.0 draft: factory takes descriptor_index:
DescriptorIndex (not tile_store: TileStore) because descriptor_dim()
lives on DescriptorIndex per C6's Public API. The contract markdown
is updated to match.
Architecture: VprCandidate.tile_id is a plain (zoom, lat, lon) tuple,
keeping _types/ (L1) free of any c6_tile_cache (L3) import per
module-layout.md. Consumers reconstruct TileId at the C6 boundary.
Excluded per task spec:
* Concrete backbones (AZ-337..AZ-340).
* FAISS HNSW retrieve wiring (AZ-341).
* DescriptorNormaliser helper (AZ-283, already shipped).
* AC-9 single-thread binding — deferred per task spec Risk 4 until the
generic compose_root thread-binding registry is in place (today
each factory owns its own, e.g. fc_factory).
Tests: 45 ACs + NFRs in tests/unit/c2_vpr/test_protocol_conformance.py
covering AC-1..AC-8, the error family, the config validation, the
factory NFR (p99 ≤ 50 ms). The legacy smoke test is removed. Full
sweep 973 passed, 2 skipped (CI-only cmake / actionlint).
Co-authored-by: Cursor <cursoragent@cursor.com>