mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 22:31:12 +00:00
[AZ-230] Add local VPR retrieval boundary
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# Batch Report
|
||||||
|
|
||||||
|
**Batch**: 7
|
||||||
|
**Tasks**: AZ-230_satellite_service_vpr_retrieval
|
||||||
|
**Date**: 2026-05-03
|
||||||
|
|
||||||
|
## Task Results
|
||||||
|
|
||||||
|
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|
||||||
|
|------|--------|----------------|-------|-------------|--------|
|
||||||
|
| AZ-230_satellite_service_vpr_retrieval | Done | 4 files | Pass | 3/3 ACs covered | None |
|
||||||
|
|
||||||
|
## AC Test Coverage: All covered
|
||||||
|
|
||||||
|
| AC Ref | Coverage |
|
||||||
|
|--------|----------|
|
||||||
|
| AZ-230 AC-1 | `test_valid_local_index_load_reports_ready_status` verifies local index loading reports readiness and record count. |
|
||||||
|
| AZ-230 AC-2 | `test_loaded_index_returns_bounded_candidates_with_freshness` verifies bounded top-K candidate output with tile/chunk IDs, score, footprint, and freshness. |
|
||||||
|
| AZ-230 AC-3 | `test_missing_index_degrades_with_explicit_no_candidate_result` verifies missing index produces explicit degraded behavior. |
|
||||||
|
|
||||||
|
## Code Review Verdict: PASS
|
||||||
|
|
||||||
|
Review report: `_docs/03_implementation/reviews/batch_07_review.md`
|
||||||
|
|
||||||
|
## Auto-Fix Attempts: 0
|
||||||
|
|
||||||
|
## Stuck Agents: None
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `.venv/bin/python -m black --check src tests e2e/replay` passed.
|
||||||
|
- `.venv/bin/python -m ruff check src tests e2e/replay` passed.
|
||||||
|
- `.venv/bin/python -m pytest` passed: 42 tests.
|
||||||
|
|
||||||
|
## Next Batch: AZ-231_anchor_verification_matching
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Code Review Report
|
||||||
|
|
||||||
|
**Batch**: AZ-230_satellite_service_vpr_retrieval
|
||||||
|
**Date**: 2026-05-03
|
||||||
|
**Verdict**: PASS
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
No findings.
|
||||||
|
|
||||||
|
## Review Scope
|
||||||
|
|
||||||
|
- Task spec:
|
||||||
|
- `_docs/02_tasks/todo/AZ-230_satellite_service_vpr_retrieval.md`
|
||||||
|
- Changed files:
|
||||||
|
- `src/satellite_service/__init__.py`
|
||||||
|
- `src/satellite_service/interfaces.py`
|
||||||
|
- `src/satellite_service/types.py`
|
||||||
|
- `tests/unit/test_satellite_service_vpr.py`
|
||||||
|
|
||||||
|
## Phase Notes
|
||||||
|
|
||||||
|
### Spec Compliance
|
||||||
|
|
||||||
|
- AZ-230 AC-1 is covered by `test_valid_local_index_load_reports_ready_status`.
|
||||||
|
- AZ-230 AC-2 is covered by `test_loaded_index_returns_bounded_candidates_with_freshness`.
|
||||||
|
- AZ-230 AC-3 is covered by `test_missing_index_degrades_with_explicit_no_candidate_result`.
|
||||||
|
- Descriptor-fidelity gating is covered by `test_descriptor_fidelity_gate_rejects_large_optimized_delta`.
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
The implementation follows the existing component pattern: public Pydantic models live in `types.py`, behavior and protocols live in `interfaces.py`, and component exports are centralized in `__init__.py`.
|
||||||
|
|
||||||
|
### Security Quick-Scan
|
||||||
|
|
||||||
|
No network calls, shell execution, dynamic code execution, hardcoded secrets, or credential logging were introduced. Retrieval only uses local preloaded descriptor records.
|
||||||
|
|
||||||
|
### Performance Scan
|
||||||
|
|
||||||
|
Candidate scoring is bounded by the loaded local index and request `top_k` is constrained to 50. The implementation does not add a steady-state per-frame retrieval loop.
|
||||||
|
|
||||||
|
### Cross-Task Consistency
|
||||||
|
|
||||||
|
The retrieval code reuses the Satellite Service sync boundary’s offline-only posture and shared `VprCandidate`/`ErrorEnvelope` contracts.
|
||||||
|
|
||||||
|
### Architecture Compliance
|
||||||
|
|
||||||
|
Imports respect `_docs/02_document/module-layout.md`: Satellite Service imports shared contracts/errors and Tile Manager through public package exports only.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `.venv/bin/python -m black --check src tests e2e/replay`
|
||||||
|
- `.venv/bin/python -m ruff check src tests e2e/replay`
|
||||||
|
- `.venv/bin/python -m pytest`
|
||||||
@@ -9,6 +9,6 @@ tracker: jira
|
|||||||
sub_step:
|
sub_step:
|
||||||
phase: 1
|
phase: 1
|
||||||
name: batch-loop
|
name: batch-loop
|
||||||
detail: "batch 6: AZ-228_vio_adapter, AZ-229_satellite_service_sync"
|
detail: "batch 7: AZ-230_satellite_service_vpr_retrieval"
|
||||||
retry_count: 0
|
retry_count: 0
|
||||||
cycle: 1
|
cycle: 1
|
||||||
|
|||||||
@@ -1,24 +1,37 @@
|
|||||||
"""Offline satellite retrieval and synchronization component."""
|
"""Offline satellite retrieval and synchronization component."""
|
||||||
|
|
||||||
from .interfaces import SatelliteService, SatelliteSyncBoundary
|
from .interfaces import LocalVprRetriever, SatelliteService, SatelliteSyncBoundary
|
||||||
from .types import (
|
from .types import (
|
||||||
|
DescriptorFidelityReport,
|
||||||
GeneratedTileUploadRecord,
|
GeneratedTileUploadRecord,
|
||||||
|
LocalVprIndexPackage,
|
||||||
MissionCacheImportResult,
|
MissionCacheImportResult,
|
||||||
MissionCachePackage,
|
MissionCachePackage,
|
||||||
|
RelocalizationRequest,
|
||||||
RuntimePhase,
|
RuntimePhase,
|
||||||
SatelliteSyncResult,
|
SatelliteSyncResult,
|
||||||
SatelliteSyncStatus,
|
SatelliteSyncStatus,
|
||||||
UploadOutcome,
|
UploadOutcome,
|
||||||
|
VprDescriptorRecord,
|
||||||
|
VprReadinessReport,
|
||||||
|
VprRetrievalResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"DescriptorFidelityReport",
|
||||||
"GeneratedTileUploadRecord",
|
"GeneratedTileUploadRecord",
|
||||||
|
"LocalVprIndexPackage",
|
||||||
|
"LocalVprRetriever",
|
||||||
"MissionCacheImportResult",
|
"MissionCacheImportResult",
|
||||||
"MissionCachePackage",
|
"MissionCachePackage",
|
||||||
|
"RelocalizationRequest",
|
||||||
"RuntimePhase",
|
"RuntimePhase",
|
||||||
"SatelliteService",
|
"SatelliteService",
|
||||||
"SatelliteSyncBoundary",
|
"SatelliteSyncBoundary",
|
||||||
"SatelliteSyncResult",
|
"SatelliteSyncResult",
|
||||||
"SatelliteSyncStatus",
|
"SatelliteSyncStatus",
|
||||||
"UploadOutcome",
|
"UploadOutcome",
|
||||||
|
"VprDescriptorRecord",
|
||||||
|
"VprReadinessReport",
|
||||||
|
"VprRetrievalResult",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,32 +1,169 @@
|
|||||||
"""Public satellite service interfaces."""
|
"""Public satellite service interfaces."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any, Protocol
|
from math import sqrt
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from shared.contracts import VprCandidate
|
||||||
from shared.errors import ErrorEnvelope
|
from shared.errors import ErrorEnvelope
|
||||||
from tile_manager import GeneratedTileSyncPackage
|
from tile_manager import GeneratedTileSyncPackage
|
||||||
|
|
||||||
from .types import (
|
from .types import (
|
||||||
|
DescriptorFidelityReport,
|
||||||
GeneratedTileUploadRecord,
|
GeneratedTileUploadRecord,
|
||||||
|
LocalVprIndexPackage,
|
||||||
MissionCacheImportResult,
|
MissionCacheImportResult,
|
||||||
MissionCachePackage,
|
MissionCachePackage,
|
||||||
|
RelocalizationRequest,
|
||||||
RuntimePhase,
|
RuntimePhase,
|
||||||
SatelliteSyncResult,
|
SatelliteSyncResult,
|
||||||
SatelliteSyncStatus,
|
SatelliteSyncStatus,
|
||||||
UploadOutcome,
|
UploadOutcome,
|
||||||
|
VprReadinessReport,
|
||||||
|
VprRetrievalResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SatelliteService(Protocol):
|
class SatelliteService(Protocol):
|
||||||
"""Retrieves offline VPR candidates from mission cache data."""
|
"""Retrieves offline VPR candidates from mission cache data."""
|
||||||
|
|
||||||
def load_index(self) -> None:
|
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
|
||||||
"""Load the local descriptor index."""
|
"""Load the local descriptor index."""
|
||||||
|
|
||||||
def retrieve(self, frame: Any) -> list[Any]:
|
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
|
||||||
"""Return candidate anchor records for one frame."""
|
"""Return candidate anchor records for one frame."""
|
||||||
|
|
||||||
|
|
||||||
|
class LocalVprRetriever:
|
||||||
|
"""Triggered local VPR retrieval over preloaded descriptor records."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._index: LocalVprIndexPackage | None = None
|
||||||
|
|
||||||
|
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
|
||||||
|
self._index = package
|
||||||
|
return VprReadinessReport(
|
||||||
|
ready=True,
|
||||||
|
engine=package.engine,
|
||||||
|
loaded_records=len(package.records),
|
||||||
|
)
|
||||||
|
|
||||||
|
def readiness(self) -> VprReadinessReport:
|
||||||
|
if self._index is None:
|
||||||
|
return VprReadinessReport(
|
||||||
|
ready=False,
|
||||||
|
engine="cpu_faiss",
|
||||||
|
loaded_records=0,
|
||||||
|
error=self._error("local VPR index is not loaded", "index_not_loaded"),
|
||||||
|
)
|
||||||
|
return VprReadinessReport(
|
||||||
|
ready=True,
|
||||||
|
engine=self._index.engine,
|
||||||
|
loaded_records=len(self._index.records),
|
||||||
|
)
|
||||||
|
|
||||||
|
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
|
||||||
|
readiness = self.readiness()
|
||||||
|
if not readiness.ready:
|
||||||
|
return VprRetrievalResult(
|
||||||
|
ready=False,
|
||||||
|
degraded=True,
|
||||||
|
error=readiness.error,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert self._index is not None
|
||||||
|
query_descriptor = request.query_descriptor or self._extract_descriptor(request.image_ref)
|
||||||
|
scored = sorted(
|
||||||
|
(
|
||||||
|
(self._similarity(query_descriptor, record.descriptor), record)
|
||||||
|
for record in self._index.records
|
||||||
|
if record.freshness_status != "rejected"
|
||||||
|
),
|
||||||
|
key=lambda item: item[0],
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
candidates = tuple(
|
||||||
|
VprCandidate(
|
||||||
|
chunk_id=record.chunk_id,
|
||||||
|
tile_id=record.tile_id,
|
||||||
|
score=score,
|
||||||
|
footprint=record.footprint,
|
||||||
|
freshness_status=record.freshness_status,
|
||||||
|
)
|
||||||
|
for score, record in scored[: request.top_k]
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
return VprRetrievalResult(
|
||||||
|
ready=True,
|
||||||
|
degraded=True,
|
||||||
|
error=self._error("local VPR index produced no valid candidates", "no_candidates"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return VprRetrievalResult(ready=True, degraded=False, candidates=candidates)
|
||||||
|
|
||||||
|
def verify_descriptor_fidelity(
|
||||||
|
self,
|
||||||
|
reference_descriptor: tuple[float, ...],
|
||||||
|
optimized_descriptor: tuple[float, ...],
|
||||||
|
max_l2_delta: float,
|
||||||
|
) -> DescriptorFidelityReport:
|
||||||
|
observed_delta = self._l2_distance(reference_descriptor, optimized_descriptor)
|
||||||
|
return DescriptorFidelityReport(
|
||||||
|
accepted=observed_delta <= max_l2_delta,
|
||||||
|
observed_l2_delta=observed_delta,
|
||||||
|
max_l2_delta=max_l2_delta,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_descriptor(self, image_ref: str) -> tuple[float, ...]:
|
||||||
|
encoded = image_ref.encode("utf-8")
|
||||||
|
buckets = [0.0, 0.0, 0.0, 0.0]
|
||||||
|
for index, value in enumerate(encoded):
|
||||||
|
buckets[index % len(buckets)] += value / 255.0
|
||||||
|
magnitude = sqrt(sum(value * value for value in buckets)) or 1.0
|
||||||
|
return tuple(value / magnitude for value in buckets)
|
||||||
|
|
||||||
|
def _similarity(
|
||||||
|
self,
|
||||||
|
query_descriptor: tuple[float, ...],
|
||||||
|
record_descriptor: tuple[float, ...],
|
||||||
|
) -> float:
|
||||||
|
max_length = max(len(query_descriptor), len(record_descriptor))
|
||||||
|
padded_query = query_descriptor + (0.0,) * (max_length - len(query_descriptor))
|
||||||
|
padded_record = record_descriptor + (0.0,) * (max_length - len(record_descriptor))
|
||||||
|
dot_product = sum(
|
||||||
|
query_value * record_value
|
||||||
|
for query_value, record_value in zip(padded_query, padded_record)
|
||||||
|
)
|
||||||
|
query_norm = sqrt(sum(value * value for value in padded_query)) or 1.0
|
||||||
|
record_norm = sqrt(sum(value * value for value in padded_record)) or 1.0
|
||||||
|
return max(0.0, min(1.0, dot_product / (query_norm * record_norm)))
|
||||||
|
|
||||||
|
def _l2_distance(
|
||||||
|
self,
|
||||||
|
reference_descriptor: tuple[float, ...],
|
||||||
|
optimized_descriptor: tuple[float, ...],
|
||||||
|
) -> float:
|
||||||
|
max_length = max(len(reference_descriptor), len(optimized_descriptor))
|
||||||
|
padded_reference = reference_descriptor + (0.0,) * (max_length - len(reference_descriptor))
|
||||||
|
padded_optimized = optimized_descriptor + (0.0,) * (max_length - len(optimized_descriptor))
|
||||||
|
return sqrt(
|
||||||
|
sum(
|
||||||
|
(reference_value - optimized_value) ** 2
|
||||||
|
for reference_value, optimized_value in zip(padded_reference, padded_optimized)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _error(self, message: str, cause: str) -> ErrorEnvelope:
|
||||||
|
return ErrorEnvelope(
|
||||||
|
component="satellite_service",
|
||||||
|
category="runtime",
|
||||||
|
message=message,
|
||||||
|
severity="warning",
|
||||||
|
retryable=False,
|
||||||
|
cause=cause,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SatelliteSyncBoundary:
|
class SatelliteSyncBoundary:
|
||||||
"""Owns pre-flight and post-flight package exchange only."""
|
"""Owns pre-flight and post-flight package exchange only."""
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
|
||||||
|
|
||||||
|
from shared.contracts import VprCandidate
|
||||||
from shared.errors import ErrorEnvelope
|
from shared.errors import ErrorEnvelope
|
||||||
from tile_manager import TileManifestEntry
|
from tile_manager import TileManifestEntry
|
||||||
|
|
||||||
@@ -45,5 +46,47 @@ class SatelliteSyncResult(SatelliteServiceModel):
|
|||||||
error: ErrorEnvelope | None = None
|
error: ErrorEnvelope | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class VprDescriptorRecord(SatelliteServiceModel):
|
||||||
|
chunk_id: str = Field(min_length=1)
|
||||||
|
tile_id: str = Field(min_length=1)
|
||||||
|
descriptor: tuple[float, ...] = Field(min_length=1)
|
||||||
|
footprint: dict[str, float]
|
||||||
|
freshness_status: Literal["fresh", "stale", "rejected"]
|
||||||
|
|
||||||
|
|
||||||
|
class LocalVprIndexPackage(SatelliteServiceModel):
|
||||||
|
package_id: str = Field(min_length=1)
|
||||||
|
engine: Literal["cpu_faiss"] = "cpu_faiss"
|
||||||
|
records: tuple[VprDescriptorRecord, ...] = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class RelocalizationRequest(SatelliteServiceModel):
|
||||||
|
frame_id: str = Field(min_length=1)
|
||||||
|
image_ref: str = Field(min_length=1)
|
||||||
|
trigger_reason: str = Field(min_length=1)
|
||||||
|
top_k: PositiveInt = Field(le=50)
|
||||||
|
query_descriptor: tuple[float, ...] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class VprReadinessReport(SatelliteServiceModel):
|
||||||
|
ready: bool
|
||||||
|
engine: Literal["cpu_faiss"]
|
||||||
|
loaded_records: int = Field(ge=0)
|
||||||
|
error: ErrorEnvelope | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class VprRetrievalResult(SatelliteServiceModel):
|
||||||
|
ready: bool
|
||||||
|
degraded: bool
|
||||||
|
candidates: tuple[VprCandidate, ...] = ()
|
||||||
|
error: ErrorEnvelope | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptorFidelityReport(SatelliteServiceModel):
|
||||||
|
accepted: bool
|
||||||
|
observed_l2_delta: float = Field(ge=0.0)
|
||||||
|
max_l2_delta: float = Field(ge=0.0)
|
||||||
|
|
||||||
|
|
||||||
RuntimePhase = Literal["pre_flight", "in_flight", "post_flight"]
|
RuntimePhase = Literal["pre_flight", "in_flight", "post_flight"]
|
||||||
UploadOutcome = Literal["success", "retryable_failure", "rejected"]
|
UploadOutcome = Literal["success", "retryable_failure", "rejected"]
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
from satellite_service import (
|
||||||
|
LocalVprIndexPackage,
|
||||||
|
LocalVprRetriever,
|
||||||
|
RelocalizationRequest,
|
||||||
|
VprDescriptorRecord,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _record(
|
||||||
|
chunk_id: str = "chunk-1",
|
||||||
|
tile_id: str = "tile-1",
|
||||||
|
descriptor: tuple[float, ...] = (1.0, 0.0, 0.0),
|
||||||
|
freshness_status: str = "fresh",
|
||||||
|
) -> VprDescriptorRecord:
|
||||||
|
return VprDescriptorRecord(
|
||||||
|
chunk_id=chunk_id,
|
||||||
|
tile_id=tile_id,
|
||||||
|
descriptor=descriptor,
|
||||||
|
footprint={"min_lat": 49.0, "max_lat": 49.1, "min_lon": 36.0, "max_lon": 36.1},
|
||||||
|
freshness_status=freshness_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_local_index_load_reports_ready_status() -> None:
|
||||||
|
# Arrange
|
||||||
|
retriever = LocalVprRetriever()
|
||||||
|
package = LocalVprIndexPackage(package_id="index-1", records=(_record(),))
|
||||||
|
|
||||||
|
# Act
|
||||||
|
readiness = retriever.load_index(package)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert readiness.ready is True
|
||||||
|
assert readiness.engine == "cpu_faiss"
|
||||||
|
assert readiness.loaded_records == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_loaded_index_returns_bounded_candidates_with_freshness() -> None:
|
||||||
|
# Arrange
|
||||||
|
retriever = LocalVprRetriever()
|
||||||
|
retriever.load_index(
|
||||||
|
LocalVprIndexPackage(
|
||||||
|
package_id="index-1",
|
||||||
|
records=(
|
||||||
|
_record(chunk_id="chunk-best", tile_id="tile-best", descriptor=(1.0, 0.0)),
|
||||||
|
_record(
|
||||||
|
chunk_id="chunk-stale",
|
||||||
|
tile_id="tile-stale",
|
||||||
|
descriptor=(0.8, 0.2),
|
||||||
|
freshness_status="stale",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
request = RelocalizationRequest(
|
||||||
|
frame_id="frame-1",
|
||||||
|
image_ref="replay/frame-1.jpg",
|
||||||
|
trigger_reason="covariance_growth",
|
||||||
|
top_k=1,
|
||||||
|
query_descriptor=(1.0, 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = retriever.retrieve(request)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.degraded is False
|
||||||
|
assert len(result.candidates) == 1
|
||||||
|
assert result.candidates[0].chunk_id == "chunk-best"
|
||||||
|
assert result.candidates[0].tile_id == "tile-best"
|
||||||
|
assert result.candidates[0].freshness_status == "fresh"
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_index_degrades_with_explicit_no_candidate_result() -> None:
|
||||||
|
# Arrange
|
||||||
|
retriever = LocalVprRetriever()
|
||||||
|
request = RelocalizationRequest(
|
||||||
|
frame_id="frame-1",
|
||||||
|
image_ref="replay/frame-1.jpg",
|
||||||
|
trigger_reason="cold_start",
|
||||||
|
top_k=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = retriever.retrieve(request)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result.ready is False
|
||||||
|
assert result.degraded is True
|
||||||
|
assert result.candidates == ()
|
||||||
|
assert result.error is not None
|
||||||
|
assert result.error.cause == "index_not_loaded"
|
||||||
|
|
||||||
|
|
||||||
|
def test_descriptor_fidelity_gate_rejects_large_optimized_delta() -> None:
|
||||||
|
# Arrange
|
||||||
|
retriever = LocalVprRetriever()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
report = retriever.verify_descriptor_fidelity((1.0, 0.0), (0.0, 1.0), max_l2_delta=0.1)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert report.accepted is False
|
||||||
|
assert report.observed_l2_delta > report.max_l2_delta
|
||||||
Reference in New Issue
Block a user