feat(perception): seal detector replay evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 14:36:55 +03:00
parent a680debfaf
commit 95ac235122
12 changed files with 1754 additions and 31 deletions
+229
View File
@@ -0,0 +1,229 @@
from __future__ import annotations
import argparse
import json
import math
from collections.abc import Iterator
from pathlib import Path
from threading import Event
import numpy as np
import pytest
from numpy.typing import NDArray
from k1link.compute.yolox_object_detector import (
YOLOX_CONFIG_SHA256,
YOLOX_MODEL_SHA256,
YOLOX_VALID_FOV_SHA256,
)
from k1link.perception.contracts import (
ClockBasis,
ModalityOutcome,
ModalityStatus,
SourceEnvelope,
TimestampBundle,
)
from k1link.perception.detector import FrozenYoloxDetectorProvider
from k1link.perception.detector_replay import run_detector_replay
from k1link.perception.detector_replay_cli import _loopback_triton_origin
from k1link.perception.detector_replay_result import (
DetectorReplayGate,
DetectorReplayResultError,
DetectorRuntimeIdentity,
read_detector_replay_result,
require_m4_detector_replay_acceptance,
)
from k1link.perception.providers import SourcePacket
class _Source:
provider_id = "ravnoves00-recorded-source/v1"
def __init__(self, packets: tuple[SourcePacket, ...]) -> None:
self._packets = packets
self.closed = False
def packets(self, stop_event: Event) -> Iterator[SourcePacket]:
try:
for packet in self._packets:
if stop_event.is_set():
return
yield packet
finally:
self.closed = True
class _Resizer:
def resize(
self,
image: NDArray[np.uint8],
width: int,
height: int,
) -> NDArray[np.uint8]:
assert image.shape == (600, 800, 3)
return np.zeros((height, width, 3), dtype=np.uint8)
class _Backend:
def infer(self, tensor: NDArray[np.float32]) -> NDArray[np.float32]:
assert tensor.shape == (1, 3, 640, 640)
output = np.zeros((1, 8400, 85), dtype=np.float32)
output[0, 0, :4] = [40.0, 30.0, math.log(10.0), math.log(10.0)]
output[0, 0, 4] = 0.9
output[0, 0, 5] = 0.9
return output
def _status() -> ModalityStatus:
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
def _packet(sequence: int, image: object) -> SourcePacket:
return SourcePacket(
envelope=SourceEnvelope(
source_id="RAVNOVES00",
session_id="20260720T065719Z_viewer_live",
frame_id=f"frame-{sequence:06d}",
sequence=sequence,
timestamps=TimestampBundle(
utc_ns=1_000 + sequence,
monotonic_ns=2_000 + sequence,
source_ns=3_000 + sequence,
clock_basis=ClockBasis.RECORDED_HOST,
),
source_age_ns=0,
binding_reason="test-recorded-source",
calibration_id="camera-1-kb4-test",
representation_id="registered-map-increment-v1",
image=_status(),
registered_point_increment=_status(),
pose=_status(),
),
image_payload=image,
registered_point_increment_payload=("points", sequence),
pose_payload=("pose", sequence),
)
def _runtime(
*,
source_mount_read_only: bool = True,
public_worker_port_added: bool = False,
same_host_tensor_transport: bool = True,
) -> DetectorRuntimeIdentity:
return DetectorRuntimeIdentity(
worker_id="worker-006",
worker_node="DESKTOP-OPJ8J04",
worker_container_id="1" * 64,
worker_image_id=f"sha256:{'2' * 64}",
triton_container_id="3" * 64,
triton_image_id=f"sha256:{'4' * 64}",
triton_model_sha256=YOLOX_MODEL_SHA256,
triton_model_config_sha256=YOLOX_CONFIG_SHA256,
valid_fov_mask_sha256=YOLOX_VALID_FOV_SHA256,
artifact_sha256="5" * 64,
code_revision="6" * 40,
source_mount_read_only=source_mount_read_only,
model_service_reused=True,
public_worker_port_added=public_worker_port_added,
same_host_tensor_transport=same_host_tensor_transport,
)
def _provider(clock_values: tuple[int, ...]) -> FrozenYoloxDetectorProvider:
return FrozenYoloxDetectorProvider(
mask=np.ones((600, 800), dtype=np.bool_),
backend=_Backend(),
resizer=_Resizer(),
clock_ns=iter(clock_values).__next__,
)
def test_replay_seals_and_reopens_exact_class_agnostic_capacity_receipt(
tmp_path: Path,
) -> None:
image = np.zeros((600, 800, 3), dtype=np.uint8)
result = run_detector_replay(
source=_Source((_packet(0, image), _packet(1, image))),
provider=_provider((100, 1_000_100, 2_000_100, 3_000_100)),
runtime=_runtime(),
output_root=tmp_path,
gate=DetectorReplayGate(expected_frames=2, minimum_end_to_end_fps=10.004),
clock_ns=iter((0, 10, 20, 30, 40, 100_000_000)).__next__,
created_at_utc="2026-08-05T12:00:00.000Z",
)
assert result.accepted is True
assert result.metrics.frame_count == 2
assert result.metrics.completed_frame_count == 2
assert result.metrics.proposal_count == 2
assert result.metrics.semantic_hint_count == 2
assert result.metrics.provider_tracklet_count == 0
assert result.metrics.end_to_end_fps == 20.0
assert result.metrics.provider_core_fps == 1000.0
assert all(frame.to_dict()["class_routing_used"] is False for frame in result.frames)
assert all(frame.proposals[0].provider_tracklet is None for frame in result.frames)
reopened = read_detector_replay_result(result.result_root)
assert reopened.result_id == result.result_id
assert reopened.receipt["accepted"] is True
assert reopened.runtime.worker_id == "worker-006"
with pytest.raises(DetectorReplayResultError, match="does not close"):
require_m4_detector_replay_acceptance(reopened)
def test_replay_seals_failed_frame_without_fabricating_missing_proposals(tmp_path: Path) -> None:
image = np.zeros((600, 800, 3), dtype=np.uint8)
source = _Source((_packet(0, image), _packet(1, "opaque-image")))
result = run_detector_replay(
source=source,
provider=_provider((100, 200, 300, 400)),
runtime=_runtime(),
output_root=tmp_path,
gate=DetectorReplayGate(expected_frames=2, minimum_end_to_end_fps=1.0),
clock_ns=iter((0, 10, 20, 30, 40, 1_000_000_000)).__next__,
created_at_utc="2026-08-05T12:01:00.000Z",
)
assert result.accepted is False
assert result.metrics.completed_frame_count == 1
assert result.metrics.failed_frame_count == 1
assert result.frames[1].outcome == "failed"
assert result.frames[1].failure_code == "DetectorProviderError"
assert result.frames[1].proposals == ()
assert source.closed is True
def test_replay_result_detects_artifact_tampering(tmp_path: Path) -> None:
image = np.zeros((600, 800, 3), dtype=np.uint8)
result = run_detector_replay(
source=_Source((_packet(0, image),)),
provider=_provider((100, 200)),
runtime=_runtime(),
output_root=tmp_path,
gate=DetectorReplayGate(expected_frames=1, minimum_end_to_end_fps=1.0),
clock_ns=iter((0, 10, 20, 1_000_000)).__next__,
created_at_utc="2026-08-05T12:02:00.000Z",
)
frames_path = result.result_root / "frames.jsonl"
row = json.loads(frames_path.read_text("utf-8"))
row["class_routing_used"] = True
frames_path.write_text(json.dumps(row) + "\n", "utf-8")
with pytest.raises(DetectorReplayResultError, match="artifact changed"):
read_detector_replay_result(result.result_root)
def test_runtime_identity_rejects_noncanonical_worker_topology() -> None:
with pytest.raises(DetectorReplayResultError, match="read-only"):
_runtime(source_mount_read_only=False)
with pytest.raises(DetectorReplayResultError, match="public worker port"):
_runtime(public_worker_port_added=True)
with pytest.raises(DetectorReplayResultError, match="worker host"):
_runtime(same_host_tensor_transport=False)
def test_m4_cli_rejects_remote_triton_tensor_transport() -> None:
assert _loopback_triton_origin("http://127.0.0.1:8000") == "http://127.0.0.1:8000"
with pytest.raises(argparse.ArgumentTypeError, match="worker-local loopback"):
_loopback_triton_origin("http://192.168.68.52:8000")
+23
View File
@@ -49,6 +49,29 @@ def test_m4_baseline_cannot_silently_select_another_source(tmp_path: Path) -> No
load_m4_baseline(path)
@pytest.mark.parametrize(
("section", "key", "value", "message"),
(
("calibration", "valid_fov_mask_sha256", "0" * 64, "calibration"),
("detector", "minimum_score", 0.51, "detector"),
("rollback", "worker_node", "worker-007", "rollback"),
),
)
def test_m4_baseline_cannot_silently_tune_frozen_execution_identity(
tmp_path: Path,
section: str,
key: str,
value: object,
message: str,
) -> None:
document = json.loads(BASELINE_PATH.read_text("utf-8"))
document[section][key] = value
path = tmp_path / "baseline.json"
path.write_text(json.dumps(document), "utf-8")
with pytest.raises(BaselineContractError, match=message):
load_m4_baseline(path)
def test_reuse_inventory_separates_primitives_from_historical_wrappers() -> None:
document = validate_reuse_inventory(REUSE_PATH)
assert document["rules"]["bulk_legacy_migration_required"] is False
+11
View File
@@ -11,6 +11,7 @@ from numpy.typing import NDArray
from k1link.compute.yolox_object_detector import (
FrozenYoloxConfig,
TritonHttpInferenceBackend,
YoloxDetection,
YoloxDetectorError,
postprocess_yolox,
@@ -173,6 +174,16 @@ def test_frozen_profile_rejects_in_place_threshold_tuning() -> None:
FrozenYoloxConfig(minimum_score=0.51)
def test_triton_transport_pins_the_frozen_model_version() -> None:
backend = TritonHttpInferenceBackend("http://127.0.0.1:8000")
try:
assert backend.path == "/v2/models/yolox_s/versions/1/infer"
finally:
backend.close()
with pytest.raises(YoloxDetectorError, match="explicit HTTP origin"):
TritonHttpInferenceBackend("http://user:secret@127.0.0.1:8000")
def test_all_4489_accepted_e46j_frames_map_to_product_contract_without_class_routing() -> None:
assert E46J_FRAMES.is_file()
image = np.zeros((600, 800, 3), dtype=np.uint8)