feat(lab): complete E30 evidence review gate
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e30_engineering_generation import (
|
||||
E30_ENGINEERING_DECISION_SCHEMA,
|
||||
E30EngineeringGenerationError,
|
||||
_validate_decision,
|
||||
)
|
||||
from k1link.web.e30_engineering_api import _validate_decisions
|
||||
|
||||
|
||||
def _item() -> dict[str, object]:
|
||||
return {
|
||||
"sequence": 0,
|
||||
"item_id": f"e30-review-item-{'a' * 64}",
|
||||
"review_key": "geometry:100:0",
|
||||
"stratum": "geometry-only",
|
||||
}
|
||||
|
||||
|
||||
def _decision(**overrides: object) -> dict[str, object]:
|
||||
value: dict[str, object] = {
|
||||
"schema_version": E30_ENGINEERING_DECISION_SCHEMA,
|
||||
"sequence": 0,
|
||||
"item_id": f"e30-review-item-{'a' * 64}",
|
||||
"review_key": "geometry:100:0",
|
||||
"source_stratum": "geometry-only",
|
||||
"verdict": "confirmed",
|
||||
"effective_stratum": "geometry-only",
|
||||
"detector_assessment": "missed-object",
|
||||
"projection_assessment": "aligned",
|
||||
"point_ownership": "object",
|
||||
"cause_code": "detector_error",
|
||||
"confidence": 0.88,
|
||||
"human_exception_required": False,
|
||||
"exception_reason": None,
|
||||
"review_prompt": None,
|
||||
"evidence_note": "Visible object has geometry but no semantic observation.",
|
||||
"review_sheet": {
|
||||
"path": "geometry-only-01.jpg",
|
||||
"sha256": "b" * 64,
|
||||
"ordinal": 1,
|
||||
},
|
||||
}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
def test_engineering_decision_supports_detector_miss_without_rewriting_a2() -> None:
|
||||
validated = _validate_decision(
|
||||
value=_decision(),
|
||||
item=_item(),
|
||||
reason_taxonomy=("detector_error", "unknown"),
|
||||
expected_sheet={
|
||||
"path": "geometry-only-01.jpg",
|
||||
"sha256": "b" * 64,
|
||||
"ordinal": 1,
|
||||
},
|
||||
)
|
||||
|
||||
assert validated["verdict"] == "confirmed"
|
||||
assert validated["effective_stratum"] == "geometry-only"
|
||||
assert validated["detector_assessment"] == "missed-object"
|
||||
|
||||
|
||||
def test_engineering_uncertainty_must_route_a_bounded_exception() -> None:
|
||||
with pytest.raises(
|
||||
E30EngineeringGenerationError,
|
||||
match="insufficient decision must route an exception",
|
||||
):
|
||||
_validate_decision(
|
||||
value=_decision(
|
||||
verdict="insufficient-evidence",
|
||||
effective_stratum=None,
|
||||
detector_assessment="insufficient-evidence",
|
||||
confidence=0.48,
|
||||
),
|
||||
item=_item(),
|
||||
reason_taxonomy=("detector_error", "unknown"),
|
||||
expected_sheet={
|
||||
"path": "geometry-only-01.jpg",
|
||||
"sha256": "b" * 64,
|
||||
"ordinal": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_engineering_api_recomputes_distributions_from_all_486_decisions() -> None:
|
||||
decisions = []
|
||||
for sequence in range(486):
|
||||
decision = _decision(
|
||||
sequence=sequence,
|
||||
item_id=f"e30-review-item-{sequence:064x}",
|
||||
review_key=f"geometry:{sequence}:0",
|
||||
)
|
||||
decisions.append(decision)
|
||||
decisions[-1] = {
|
||||
**decisions[-1],
|
||||
"verdict": "insufficient-evidence",
|
||||
"effective_stratum": None,
|
||||
"detector_assessment": "insufficient-evidence",
|
||||
"projection_assessment": "not-assessable",
|
||||
"point_ownership": "insufficient-evidence",
|
||||
"cause_code": "unknown",
|
||||
"confidence": 0.48,
|
||||
"human_exception_required": True,
|
||||
"exception_reason": "ambiguity",
|
||||
"review_prompt": {
|
||||
"question": "Is this an occupied physical object?",
|
||||
"focus": "Inspect the selected white LiDAR cluster.",
|
||||
"effects": {
|
||||
"object-present": "Retain occupied geometry.",
|
||||
"background-or-noise": "Reject the cluster.",
|
||||
"insufficient-evidence": "Keep the item unknown.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
summary, causes = _validate_decisions(decisions)
|
||||
|
||||
assert summary["reviewed_item_count"] == 486
|
||||
assert summary["human_exception_count"] == 1
|
||||
assert summary["verdict_distribution"] == {
|
||||
"confirmed": 485,
|
||||
"insufficient-evidence": 1,
|
||||
}
|
||||
assert causes["reasons"] == [
|
||||
{"reason_code": "detector_error", "count": 485},
|
||||
{"reason_code": "unknown", "count": 1},
|
||||
]
|
||||
|
||||
|
||||
def test_engineering_exception_requires_a_specific_review_prompt() -> None:
|
||||
with pytest.raises(
|
||||
E30EngineeringGenerationError,
|
||||
match="exception review prompt is invalid",
|
||||
):
|
||||
_validate_decision(
|
||||
value=_decision(
|
||||
verdict="insufficient-evidence",
|
||||
effective_stratum=None,
|
||||
detector_assessment="insufficient-evidence",
|
||||
projection_assessment="not-assessable",
|
||||
point_ownership="insufficient-evidence",
|
||||
cause_code="unknown",
|
||||
confidence=0.48,
|
||||
human_exception_required=True,
|
||||
exception_reason="ambiguity",
|
||||
),
|
||||
item=_item(),
|
||||
reason_taxonomy=("detector_error", "unknown"),
|
||||
expected_sheet={
|
||||
"path": "geometry-only-01.jpg",
|
||||
"sha256": "b" * 64,
|
||||
"ordinal": 1,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,344 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter
|
||||
|
||||
from k1link.compute.e30_human_review import (
|
||||
E30HumanReviewConflictError,
|
||||
E30HumanReviewIntegrityError,
|
||||
E30HumanReviewStore,
|
||||
E30HumanReviewValidationError,
|
||||
E30ReviewSubject,
|
||||
E30ReviewSubstrate,
|
||||
)
|
||||
from k1link.web import e30_human_review_api
|
||||
from k1link.web.e30_human_review_api import build_e30_human_review_router
|
||||
|
||||
|
||||
def _endpoint(router: APIRouter, path: str, method: str) -> Any:
|
||||
for route in router.routes:
|
||||
if (
|
||||
getattr(route, "path", None) == path
|
||||
and method in getattr(route, "methods", set())
|
||||
):
|
||||
return route.endpoint
|
||||
raise AssertionError(f"{method} endpoint {path} not found")
|
||||
|
||||
|
||||
def _substrate() -> E30ReviewSubstrate:
|
||||
return E30ReviewSubstrate(
|
||||
materialization_id=f"e30-materialization-{'a' * 64}",
|
||||
materialization_identity_sha256="a" * 64,
|
||||
review_pack_id=f"e30-review-pack-{'b' * 64}",
|
||||
review_items_sha256="c" * 64,
|
||||
reason_taxonomy=(),
|
||||
subjects=(
|
||||
E30ReviewSubject(
|
||||
item_id=f"e30-review-item-{'d' * 64}",
|
||||
sequence=0,
|
||||
source_stratum="geometry-only",
|
||||
),
|
||||
E30ReviewSubject(
|
||||
item_id=f"e30-review-item-{'e' * 64}",
|
||||
sequence=1,
|
||||
source_stratum="unknown",
|
||||
),
|
||||
),
|
||||
engineering_generation_id=f"e30-engineering-generation-{'f' * 64}",
|
||||
)
|
||||
|
||||
|
||||
def _store(tmp_path: Path) -> E30HumanReviewStore:
|
||||
return E30HumanReviewStore(
|
||||
draft_root=tmp_path / "drafts",
|
||||
generation_root=tmp_path / "generations",
|
||||
)
|
||||
|
||||
|
||||
def test_exception_review_is_generation_bound_and_resumable(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
substrate = _substrate()
|
||||
store = _store(tmp_path)
|
||||
|
||||
created = store.create_or_resume(substrate=substrate, reviewer_id="DC")
|
||||
resumed = store.create_or_resume(substrate=substrate, reviewer_id="DC")
|
||||
|
||||
assert resumed["draft_id"] == created["draft_id"]
|
||||
assert resumed["engineering_generation_id"] == (
|
||||
substrate.engineering_generation_id
|
||||
)
|
||||
assert resumed["item_count"] == 2
|
||||
assert resumed["reviewed_item_count"] == 0
|
||||
|
||||
|
||||
def test_decisions_are_append_only_idempotent_and_superseding(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
substrate = _substrate()
|
||||
store = _store(tmp_path)
|
||||
draft_id = str(
|
||||
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
|
||||
)
|
||||
subject = substrate.subjects[0]
|
||||
|
||||
first = store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=subject.item_id,
|
||||
expected_revision=0,
|
||||
idempotency_key="decision-001",
|
||||
disposition="object-present",
|
||||
notes=None,
|
||||
)
|
||||
replay = store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=subject.item_id,
|
||||
expected_revision=0,
|
||||
idempotency_key="decision-001",
|
||||
disposition="object-present",
|
||||
notes=None,
|
||||
)
|
||||
changed = store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=subject.item_id,
|
||||
expected_revision=1,
|
||||
idempotency_key="decision-002",
|
||||
disposition="background-or-noise",
|
||||
notes="Static façade points.",
|
||||
)
|
||||
|
||||
assert first["revision"] == replay["revision"] == 1
|
||||
assert changed["revision"] == 2
|
||||
assert changed["reviewed_item_count"] == 1
|
||||
assert changed["disposition_distribution"] == {
|
||||
"background-or-noise": 1
|
||||
}
|
||||
assert len(
|
||||
(
|
||||
tmp_path / "drafts" / draft_id / "events.jsonl"
|
||||
).read_text().splitlines()
|
||||
) == 2
|
||||
|
||||
|
||||
def test_decision_rejects_unknown_disposition(tmp_path: Path) -> None:
|
||||
substrate = _substrate()
|
||||
store = _store(tmp_path)
|
||||
draft_id = str(
|
||||
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
|
||||
)
|
||||
|
||||
with pytest.raises(E30HumanReviewValidationError):
|
||||
store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=substrate.subjects[0].item_id,
|
||||
expected_revision=0,
|
||||
idempotency_key="decision-invalid",
|
||||
disposition="invented", # type: ignore[arg-type]
|
||||
notes=None,
|
||||
)
|
||||
|
||||
|
||||
def test_finalization_requires_every_exception_and_freezes_generation(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
substrate = _substrate()
|
||||
store = _store(tmp_path)
|
||||
draft_id = str(
|
||||
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
|
||||
)
|
||||
first = store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=substrate.subjects[0].item_id,
|
||||
expected_revision=0,
|
||||
idempotency_key="decision-001",
|
||||
disposition="object-present",
|
||||
notes=None,
|
||||
)
|
||||
with pytest.raises(E30HumanReviewConflictError, match="coverage is incomplete"):
|
||||
store.finalize(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
expected_revision=int(first["revision"]),
|
||||
)
|
||||
|
||||
complete = store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=substrate.subjects[1].item_id,
|
||||
expected_revision=1,
|
||||
idempotency_key="decision-002",
|
||||
disposition="insufficient-evidence",
|
||||
notes="Occluded.",
|
||||
)
|
||||
generation = store.finalize(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
expected_revision=int(complete["revision"]),
|
||||
)
|
||||
|
||||
assert generation["human_review_complete"] is True
|
||||
assert generation["lab_published"] is False
|
||||
assert generation["engineering_generation_id"] == (
|
||||
substrate.engineering_generation_id
|
||||
)
|
||||
assert generation["disposition_distribution"] == {
|
||||
"insufficient-evidence": 1,
|
||||
"object-present": 1,
|
||||
}
|
||||
generation_id = str(generation["generation_id"])
|
||||
decision_rows = [
|
||||
json.loads(line)
|
||||
for line in (
|
||||
tmp_path
|
||||
/ "generations"
|
||||
/ generation_id
|
||||
/ "review-decisions.jsonl"
|
||||
).read_text().splitlines()
|
||||
]
|
||||
assert [row["disposition"] for row in decision_rows] == [
|
||||
"object-present",
|
||||
"insufficient-evidence",
|
||||
]
|
||||
|
||||
repeated = store.finalize(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
expected_revision=2,
|
||||
)
|
||||
assert repeated["generation_id"] == generation_id
|
||||
with pytest.raises(E30HumanReviewConflictError, match="finalized"):
|
||||
store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=substrate.subjects[0].item_id,
|
||||
expected_revision=2,
|
||||
idempotency_key="decision-after-finalize",
|
||||
disposition="object-present",
|
||||
notes=None,
|
||||
)
|
||||
|
||||
|
||||
def test_event_and_generation_tampering_fail_closed(tmp_path: Path) -> None:
|
||||
substrate = _substrate()
|
||||
store = _store(tmp_path)
|
||||
draft_id = str(
|
||||
store.create_or_resume(substrate=substrate, reviewer_id="DC")["draft_id"]
|
||||
)
|
||||
store.record_decision(
|
||||
draft_id=draft_id,
|
||||
substrate=substrate,
|
||||
item_id=substrate.subjects[0].item_id,
|
||||
expected_revision=0,
|
||||
idempotency_key="decision-001",
|
||||
disposition="object-present",
|
||||
notes=None,
|
||||
)
|
||||
events_path = tmp_path / "drafts" / draft_id / "events.jsonl"
|
||||
event = json.loads(events_path.read_text())
|
||||
event["disposition"] = "background-or-noise"
|
||||
events_path.write_text(json.dumps(event) + "\n")
|
||||
|
||||
with pytest.raises(E30HumanReviewIntegrityError):
|
||||
store.get(draft_id=draft_id, substrate=substrate)
|
||||
|
||||
|
||||
def test_http_lifecycle_is_bound_to_the_engineering_exception_queue(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
source_substrate = _substrate()
|
||||
generation_id = str(source_substrate.engineering_generation_id)
|
||||
monkeypatch.setattr(
|
||||
e30_human_review_api,
|
||||
"load_verified_e30_review",
|
||||
lambda **_: ({}, (), source_substrate),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
e30_human_review_api,
|
||||
"load_verified_e30_engineering_generation",
|
||||
lambda **_: (
|
||||
{
|
||||
"human_exceptions": [
|
||||
{"item_id": subject.item_id}
|
||||
for subject in source_substrate.subjects
|
||||
]
|
||||
},
|
||||
(),
|
||||
),
|
||||
)
|
||||
router = build_e30_human_review_router(
|
||||
materialization_root_provider=lambda: tmp_path / "materializations",
|
||||
review_pack_root_provider=lambda: tmp_path / "review-packs",
|
||||
engineering_generation_root_provider=lambda: (
|
||||
tmp_path / "engineering-generations"
|
||||
),
|
||||
draft_root_provider=lambda: tmp_path / "drafts",
|
||||
generation_root_provider=lambda: tmp_path / "generations",
|
||||
)
|
||||
create = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e30/reviews/{result_id}/human-review",
|
||||
"POST",
|
||||
)
|
||||
decide = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/e30/reviews/{result_id}/human-review/"
|
||||
"{draft_id}/decisions/{item_id}"
|
||||
),
|
||||
"PUT",
|
||||
)
|
||||
finalize = _endpoint(
|
||||
router,
|
||||
(
|
||||
"/api/v1/laboratory/e30/reviews/{result_id}/human-review/"
|
||||
"{draft_id}/finalize"
|
||||
),
|
||||
"POST",
|
||||
)
|
||||
|
||||
created = create(
|
||||
result_id=source_substrate.materialization_id,
|
||||
request=e30_human_review_api.E30HumanReviewCreateRequest(
|
||||
reviewer_id="DC",
|
||||
engineering_generation_id=generation_id,
|
||||
),
|
||||
)
|
||||
draft = created
|
||||
for revision, subject in enumerate(source_substrate.subjects):
|
||||
draft = decide(
|
||||
result_id=source_substrate.materialization_id,
|
||||
draft_id=draft["draft_id"],
|
||||
item_id=subject.item_id,
|
||||
engineering_generation_id=generation_id,
|
||||
request=e30_human_review_api.E30HumanReviewDecisionRequest(
|
||||
expected_revision=revision,
|
||||
idempotency_key=f"http-{revision}",
|
||||
disposition=(
|
||||
"object-present"
|
||||
if revision == 0
|
||||
else "insufficient-evidence"
|
||||
),
|
||||
notes=None,
|
||||
),
|
||||
)
|
||||
|
||||
finalized = finalize(
|
||||
result_id=source_substrate.materialization_id,
|
||||
draft_id=draft["draft_id"],
|
||||
engineering_generation_id=generation_id,
|
||||
request=e30_human_review_api.E30HumanReviewFinalizeRequest(
|
||||
expected_revision=draft["revision"],
|
||||
confirm_generation=True,
|
||||
),
|
||||
)
|
||||
assert finalized["draft"]["state"] == "finalized"
|
||||
@@ -0,0 +1,394 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from k1link.compute import e30_materialization as materialization
|
||||
from k1link.compute.e30_materialization import build_e30_materialization
|
||||
from k1link.compute.semantic_geometry_fusion import (
|
||||
CameraGeometryFusionProfile,
|
||||
_projection_profile,
|
||||
_semantic_support,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
project_map_points_kb4,
|
||||
)
|
||||
from k1link.web.e30_review_api import build_e30_review_router
|
||||
|
||||
|
||||
def _canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _endpoint(router: APIRouter, path: str) -> object:
|
||||
for route in router.routes:
|
||||
if (
|
||||
isinstance(route, APIRoute)
|
||||
and route.path == path
|
||||
and route.methods is not None
|
||||
and "GET" in route.methods
|
||||
):
|
||||
return route.endpoint
|
||||
raise AssertionError(f"GET {path} route is missing")
|
||||
|
||||
|
||||
class _FakeSource:
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root
|
||||
self.pack_id = root.name
|
||||
self.arrays = {
|
||||
"cloud_offsets": np.asarray([0, 4], dtype=np.int64),
|
||||
"cloud_points_map": np.asarray(
|
||||
[
|
||||
[0.00, 0.00, 2.0],
|
||||
[0.10, 0.00, 2.0],
|
||||
[0.20, 0.00, 2.0],
|
||||
[1.50, 1.50, 2.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
"pose_positions_map": np.zeros((1, 3), dtype=np.float64),
|
||||
"pose_quaternions_map_from_lidar": np.asarray(
|
||||
[[0.0, 0.0, 0.0, 1.0]],
|
||||
dtype=np.float64,
|
||||
),
|
||||
"sample_available": np.asarray([True], dtype=np.bool_),
|
||||
"source_frame_indices": np.asarray([10], dtype=np.int64),
|
||||
"session_seconds": np.asarray([12.5], dtype=np.float64),
|
||||
"intrinsic_fx_fy_cx_cy": np.asarray(
|
||||
[100.0, 100.0, 50.0, 50.0],
|
||||
dtype=np.float64,
|
||||
),
|
||||
"distortion_kb4": np.zeros(4, dtype=np.float64),
|
||||
"t_camera_from_lidar": np.eye(4, dtype=np.float64),
|
||||
}
|
||||
self.identity: dict[str, Any] = {
|
||||
"session_id": "source-session",
|
||||
"frame_count": 1,
|
||||
"point_count": 4,
|
||||
"source_id": "sensor.camera.right",
|
||||
"camera_slot": "camera_1",
|
||||
"projection": {"width": 100, "height": 100},
|
||||
}
|
||||
self.manifest = {"artifact": {"sha256": "1" * 64}}
|
||||
|
||||
@property
|
||||
def frame_count(self) -> int:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def point_count(self) -> int:
|
||||
return 4
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _FakeSurface:
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = root
|
||||
self.model_id = root.name
|
||||
self.identity: dict[str, Any] = {
|
||||
"source_pack_id": "e10-lidar-pack-" + "b" * 64,
|
||||
"frame_count": 1,
|
||||
"point_count": 4,
|
||||
}
|
||||
self.arrays = {
|
||||
"frame_valid": np.asarray([True], dtype=np.bool_),
|
||||
"point_class": np.asarray([2, 2, 2, 1], dtype=np.uint8),
|
||||
"point_height_m": np.asarray([0.5, 0.5, 0.5, 0.0], dtype=np.float32),
|
||||
}
|
||||
self.manifest = {
|
||||
"artifacts": [
|
||||
{"role": "local-surface", "sha256": "2" * 64},
|
||||
]
|
||||
}
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _write_source_tree(tmp_path: Path) -> tuple[dict[str, Path], str]:
|
||||
source_pack_id = "e10-lidar-pack-" + "b" * 64
|
||||
local_surface_id = "k1-local-surface-" + "c" * 64
|
||||
source_result_identity = {"schema_version": "test-source/v1"}
|
||||
source_result_identity_sha256 = hashlib.sha256(
|
||||
_canonical(source_result_identity)
|
||||
).hexdigest()
|
||||
source_result_id = (
|
||||
f"e10-integrated-perception-{source_result_identity_sha256}"
|
||||
)
|
||||
|
||||
roots = {
|
||||
"e29": tmp_path / "e29",
|
||||
"source_results": tmp_path / "source-results",
|
||||
"source_packs": tmp_path / "source-packs",
|
||||
"surfaces": tmp_path / "surfaces",
|
||||
"reviews": tmp_path / "reviews",
|
||||
"output": tmp_path / "output",
|
||||
}
|
||||
for root in roots.values():
|
||||
root.mkdir()
|
||||
(roots["source_packs"] / source_pack_id).mkdir()
|
||||
(roots["surfaces"] / local_surface_id).mkdir()
|
||||
|
||||
source_result = roots["source_results"] / source_result_id
|
||||
source_result.mkdir()
|
||||
result_document = {
|
||||
"result_id": source_result_id,
|
||||
"identity_sha256": source_result_identity_sha256,
|
||||
"identity": source_result_identity,
|
||||
}
|
||||
(source_result / "result.json").write_bytes(_canonical(result_document))
|
||||
fusion_frame = {
|
||||
"schema_version": "missioncore.e10-fusion-frame/v1",
|
||||
"frame_index": 0,
|
||||
"source_frame_index": 10,
|
||||
"session_seconds": 12.5,
|
||||
"objects": [
|
||||
{
|
||||
"source_track_id": 7,
|
||||
"track_id": 70,
|
||||
"label": "car",
|
||||
"association_group": "vehicle",
|
||||
"score": 0.9,
|
||||
"bbox_xyxy": [40.0, 40.0, 70.0, 60.0],
|
||||
"cuboid_status": "observed",
|
||||
"camera_motion_state": "static",
|
||||
"camera_motion_confidence": 0.8,
|
||||
"motion_state": "unknown",
|
||||
"motion_status": "test",
|
||||
}
|
||||
],
|
||||
}
|
||||
fusion_path = source_result / "fusion-frames.jsonl"
|
||||
fusion_path.write_bytes(_canonical(fusion_frame) + b"\n")
|
||||
|
||||
source = _FakeSource(roots["source_packs"] / source_pack_id)
|
||||
profile = CameraGeometryFusionProfile()
|
||||
points = np.asarray(source.arrays["cloud_points_map"], dtype=np.float64)
|
||||
projected = project_map_points_kb4(
|
||||
points,
|
||||
position_map_xyz=(0.0, 0.0, 0.0),
|
||||
orientation_map_from_lidar_xyzw=(0.0, 0.0, 0.0, 1.0),
|
||||
profile=_projection_profile(source), # type: ignore[arg-type]
|
||||
)
|
||||
snapshot = _semantic_support(
|
||||
fusion_frame["objects"][0],
|
||||
projected=projected,
|
||||
frame_points_map=points,
|
||||
point_class=np.asarray([2, 2, 2, 1], dtype=np.uint8),
|
||||
point_height_m=np.asarray([0.5, 0.5, 0.5, 0.0], dtype=np.float32),
|
||||
source_available=True,
|
||||
surface_valid=True,
|
||||
profile=profile,
|
||||
).document
|
||||
assert snapshot["geometry_status"] == "agree"
|
||||
|
||||
e29_identity = {
|
||||
"schema_version": "missioncore.e29-camera-geometry-fusion/v1",
|
||||
"source_result_id": source_result_id,
|
||||
"source_fusion_frames_sha256": _sha256(fusion_path),
|
||||
"source_pack_id": source_pack_id,
|
||||
"local_surface_model_id": local_surface_id,
|
||||
"frame_count": 1,
|
||||
"profile": profile.to_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
e29_identity_sha256 = hashlib.sha256(_canonical(e29_identity)).hexdigest()
|
||||
e29_result_id = f"e29-camera-geometry-{e29_identity_sha256}"
|
||||
e29_result = roots["e29"] / e29_result_id
|
||||
e29_result.mkdir()
|
||||
e29_manifest = {
|
||||
"schema_version": "missioncore.e29-camera-geometry-fusion/v1",
|
||||
"result_id": e29_result_id,
|
||||
"identity_sha256": e29_identity_sha256,
|
||||
"identity": e29_identity,
|
||||
}
|
||||
(e29_result / "manifest.json").write_bytes(_canonical(e29_manifest))
|
||||
|
||||
review_item_id = "e30-review-item-" + "d" * 64
|
||||
review_item = {
|
||||
"schema_version": "missioncore.e30-evidence-review-item/v1",
|
||||
"sequence": 0,
|
||||
"item_id": review_item_id,
|
||||
"review_key": "semantic:0:0",
|
||||
"stratum": "agree",
|
||||
"range_bucket": "near",
|
||||
"evidence_binding": {
|
||||
"frame_index": 0,
|
||||
"source_frame_index": 10,
|
||||
"session_seconds": 12.5,
|
||||
},
|
||||
"e29_locator": {
|
||||
"kind": "semantic-observation",
|
||||
"observation_index": 0,
|
||||
},
|
||||
"e29_snapshot": snapshot,
|
||||
"review": {"state": "unreviewed", "reason_code": None, "notes": None},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
review_items = _canonical(review_item) + b"\n"
|
||||
review_identity = {
|
||||
"source": {
|
||||
"e29_result_id": e29_result_id,
|
||||
"e29_identity_sha256": e29_identity_sha256,
|
||||
"camera_result_id": source_result_id,
|
||||
"lidar_pack_id": source_pack_id,
|
||||
"local_surface_model_id": local_surface_id,
|
||||
},
|
||||
"reason_taxonomy": [
|
||||
"no_lidar_observation",
|
||||
"outside_lidar_support",
|
||||
"outside_camera_fov",
|
||||
"time_mismatch",
|
||||
"semantic_mismatch",
|
||||
"geometry_mismatch",
|
||||
"insufficient_evidence",
|
||||
"other",
|
||||
],
|
||||
}
|
||||
review_identity_sha256 = hashlib.sha256(_canonical(review_identity)).hexdigest()
|
||||
review_result_id = f"e30-review-pack-{review_identity_sha256}"
|
||||
review_root = roots["reviews"] / review_result_id
|
||||
review_root.mkdir()
|
||||
items_path = review_root / "review-items.jsonl"
|
||||
items_path.write_bytes(review_items)
|
||||
review_manifest = {
|
||||
"schema_version": "missioncore.e30-evidence-review-pack/v1",
|
||||
"result_id": review_result_id,
|
||||
"identity_sha256": review_identity_sha256,
|
||||
"identity": review_identity,
|
||||
"human_review_complete": False,
|
||||
"lab_published": False,
|
||||
"selected_item_count": 1,
|
||||
"artifacts": [
|
||||
{
|
||||
"role": "review-items",
|
||||
"path": items_path.name,
|
||||
"byte_length": items_path.stat().st_size,
|
||||
"sha256": _sha256(items_path),
|
||||
}
|
||||
],
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
(review_root / "manifest.json").write_bytes(_canonical(review_manifest))
|
||||
roots["review_root"] = review_root
|
||||
return roots, review_item_id
|
||||
|
||||
|
||||
def test_materialization_replays_exact_support_and_publishes_point_indices(
|
||||
tmp_path: Path,
|
||||
monkeypatch: Any,
|
||||
) -> None:
|
||||
roots, review_item_id = _write_source_tree(tmp_path)
|
||||
monkeypatch.setattr(materialization, "E10LidarFieldSource", _FakeSource)
|
||||
monkeypatch.setattr(materialization, "K1LocalSurfaceV1", _FakeSurface)
|
||||
|
||||
result = build_e30_materialization(
|
||||
review_pack_root=roots["review_root"],
|
||||
e29_root=roots["e29"],
|
||||
source_result_root=roots["source_results"],
|
||||
source_pack_root=roots["source_packs"],
|
||||
local_surface_root=roots["surfaces"],
|
||||
output_root=roots["output"],
|
||||
)
|
||||
|
||||
assert result.manifest["item_count"] == 1
|
||||
assert result.manifest["human_review_complete"] is False
|
||||
index = json.loads(
|
||||
(result.result_root / "materialized-items.jsonl").read_text()
|
||||
)
|
||||
assert index["item_id"] == review_item_id
|
||||
assert index["materialization"]["selected_point_count"] == 3
|
||||
assert index["materialization"]["source_reprojection_required"] is False
|
||||
artifact = result.result_root / index["artifact"]["path"]
|
||||
with np.load(artifact, allow_pickle=False) as arrays:
|
||||
assert arrays["selected_source_indices"].tolist() == [0, 1, 2]
|
||||
assert arrays["projected_selected_mask"].sum() == 3
|
||||
assert arrays["projected_candidate_mask"].sum() >= 3
|
||||
|
||||
|
||||
def test_e30_review_api_exposes_verified_read_only_evidence(
|
||||
tmp_path: Path,
|
||||
monkeypatch: Any,
|
||||
) -> None:
|
||||
roots, review_item_id = _write_source_tree(tmp_path)
|
||||
monkeypatch.setattr(materialization, "E10LidarFieldSource", _FakeSource)
|
||||
monkeypatch.setattr(materialization, "K1LocalSurfaceV1", _FakeSurface)
|
||||
result = build_e30_materialization(
|
||||
review_pack_root=roots["review_root"],
|
||||
e29_root=roots["e29"],
|
||||
source_result_root=roots["source_results"],
|
||||
source_pack_root=roots["source_packs"],
|
||||
local_surface_root=roots["surfaces"],
|
||||
output_root=roots["output"],
|
||||
)
|
||||
router = build_e30_review_router(
|
||||
materialization_root_provider=lambda: roots["output"],
|
||||
review_pack_root_provider=lambda: roots["reviews"],
|
||||
)
|
||||
catalog_route = _endpoint(router, "/api/v1/laboratory/e30/reviews")
|
||||
items_route = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e30/reviews/{result_id}/items",
|
||||
)
|
||||
detail_route = _endpoint(
|
||||
router,
|
||||
"/api/v1/laboratory/e30/reviews/{result_id}/items/{item_id}",
|
||||
)
|
||||
|
||||
catalog = catalog_route(limit=1) # type: ignore[operator]
|
||||
items = items_route( # type: ignore[operator]
|
||||
result_id=result.result_id,
|
||||
stratum="agree",
|
||||
limit=48,
|
||||
cursor=0,
|
||||
)
|
||||
detail = detail_route( # type: ignore[operator]
|
||||
result_id=result.result_id,
|
||||
item_id=review_item_id,
|
||||
)
|
||||
|
||||
assert catalog["configured"] is True
|
||||
assert catalog["items"][0]["access"] == "read-only"
|
||||
assert catalog["items"][0]["stratum_counts"]["agree"] == 1
|
||||
assert items["total"] == 1
|
||||
assert items["items"][0]["item_id"] == review_item_id
|
||||
assert detail["item"]["selected"]["source_indices"] == [0, 1, 2]
|
||||
assert detail["item"]["projection"]["selected_mask"] == [1, 1, 1]
|
||||
assert str(tmp_path) not in repr(catalog)
|
||||
assert str(tmp_path) not in repr(items)
|
||||
assert str(tmp_path) not in repr(detail)
|
||||
|
||||
artifact = result.result_root / "items" / f"{review_item_id}.npz"
|
||||
artifact.write_bytes(artifact.read_bytes() + b"changed")
|
||||
tampered = catalog_route(limit=1) # type: ignore[operator]
|
||||
assert tampered["candidate_total"] == 1
|
||||
assert tampered["invalid_total"] == 1
|
||||
assert tampered["items"] == []
|
||||
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e30_review_pack import (
|
||||
E30_REASON_TAXONOMY,
|
||||
E30ReviewPackError,
|
||||
E30ReviewSelectionProfile,
|
||||
build_e30_review_pack,
|
||||
)
|
||||
from k1link.compute.semantic_geometry_fusion import (
|
||||
CAMERA_GEOMETRY_FRAME_SCHEMA,
|
||||
CAMERA_GEOMETRY_FUSION_SCHEMA,
|
||||
CAMERA_GEOMETRY_REPORT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
def _canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _observation(status: str, index: int) -> dict[str, object]:
|
||||
return {
|
||||
"track_id": index,
|
||||
"label": "car" if index % 2 else "person",
|
||||
"association_group": "vehicle" if index % 2 else "person",
|
||||
"geometry_status": status,
|
||||
"geometry_reason": f"reason-{status}",
|
||||
"range_m": None if status in {"single-source-camera", "unknown"} else 2.5 + index,
|
||||
"support": {"connected_occupied_points": 0},
|
||||
}
|
||||
|
||||
|
||||
def _source_result(root: Path) -> Path:
|
||||
result_id = "e29-camera-geometry-" + "a" * 64
|
||||
result = root / result_id
|
||||
result.mkdir()
|
||||
frames = [
|
||||
{
|
||||
"schema_version": CAMERA_GEOMETRY_FRAME_SCHEMA,
|
||||
"frame_index": 0,
|
||||
"source_frame_index": 10,
|
||||
"session_seconds": 1.0,
|
||||
"semantic_observations": [
|
||||
_observation("agree", 1),
|
||||
_observation("single-source-camera", 2),
|
||||
_observation("conflict", 3),
|
||||
_observation("unknown", 4),
|
||||
],
|
||||
"geometry_only_occupied": [
|
||||
{
|
||||
"geometry_status": "single-source-geometry",
|
||||
"nearest_range_m": 8.0,
|
||||
"point_count": 10,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"schema_version": CAMERA_GEOMETRY_FRAME_SCHEMA,
|
||||
"frame_index": 1,
|
||||
"source_frame_index": 11,
|
||||
"session_seconds": 2.0,
|
||||
"semantic_observations": [
|
||||
_observation("agree", 5),
|
||||
_observation("single-source-camera", 6),
|
||||
_observation("conflict", 7),
|
||||
_observation("unknown", 8),
|
||||
],
|
||||
"geometry_only_occupied": [
|
||||
{
|
||||
"geometry_status": "single-source-geometry",
|
||||
"nearest_range_m": 2.0,
|
||||
"point_count": 12,
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
frames_path = result / "camera-geometry-frames.jsonl"
|
||||
frames_path.write_bytes(b"".join(_canonical(frame) + b"\n" for frame in frames))
|
||||
|
||||
identity: dict[str, Any] = {
|
||||
"frame_count": 2,
|
||||
"timeline_start_seconds": 1.0,
|
||||
"timeline_end_seconds": 2.0,
|
||||
"source_result_id": "e10-integrated-perception-" + "b" * 64,
|
||||
"source_pack_id": "e10-lidar-pack-" + "c" * 64,
|
||||
"local_surface_model_id": "k1-local-surface-" + "d" * 64,
|
||||
"profile": {"profile_id": "camera-first-local-surface-validation/v1"},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
report = {
|
||||
"schema_version": CAMERA_GEOMETRY_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"status": "diagnostic-replay-complete",
|
||||
"ground_truth": False,
|
||||
"identity": identity,
|
||||
"metrics": {
|
||||
"semantic_observations": {
|
||||
"geometry_status": {
|
||||
"agree": 2,
|
||||
"single-source-camera": 2,
|
||||
"conflict": 2,
|
||||
"unknown": 2,
|
||||
}
|
||||
},
|
||||
"geometry_only_occupied": {"cluster_count": 2},
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
report_path = result / "camera-geometry-report.json"
|
||||
report_path.write_bytes(_canonical(report))
|
||||
identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest()
|
||||
result_id = f"e29-camera-geometry-{identity_sha256}"
|
||||
result_with_identity = root / result_id
|
||||
result.rename(result_with_identity)
|
||||
result = result_with_identity
|
||||
frames_path = result / frames_path.name
|
||||
report_path = result / report_path.name
|
||||
report["result_id"] = result_id
|
||||
report_path.write_bytes(_canonical(report))
|
||||
manifest = {
|
||||
"schema_version": CAMERA_GEOMETRY_FUSION_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"ground_truth": False,
|
||||
"artifacts": [
|
||||
{
|
||||
"role": "camera-geometry-frames",
|
||||
"path": frames_path.name,
|
||||
"byte_length": frames_path.stat().st_size,
|
||||
"sha256": _sha256(frames_path),
|
||||
},
|
||||
{
|
||||
"role": "camera-geometry-report",
|
||||
"path": report_path.name,
|
||||
"byte_length": report_path.stat().st_size,
|
||||
"sha256": _sha256(report_path),
|
||||
},
|
||||
],
|
||||
}
|
||||
(result / "manifest.json").write_bytes(_canonical(manifest))
|
||||
return result
|
||||
|
||||
|
||||
def test_review_pack_binds_all_strata_and_keeps_human_decision_open(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = _source_result(tmp_path)
|
||||
profile = E30ReviewSelectionProfile(
|
||||
agree_maximum=1,
|
||||
camera_only_maximum=1,
|
||||
unknown_maximum=1,
|
||||
geometry_only_maximum=1,
|
||||
temporal_bins=2,
|
||||
)
|
||||
|
||||
first = build_e30_review_pack(
|
||||
e29_result_root=source,
|
||||
output_root=tmp_path / "review-packs",
|
||||
profile=profile,
|
||||
)
|
||||
second = build_e30_review_pack(
|
||||
e29_result_root=source,
|
||||
output_root=tmp_path / "review-packs",
|
||||
profile=profile,
|
||||
)
|
||||
|
||||
assert first.result_id == second.result_id
|
||||
assert first.manifest["human_review_complete"] is False
|
||||
assert first.manifest["lab_published"] is False
|
||||
assert first.manifest["source_counts"] == {
|
||||
"agree": 2,
|
||||
"camera-only": 2,
|
||||
"conflict": 2,
|
||||
"geometry-only": 2,
|
||||
"unknown": 2,
|
||||
}
|
||||
assert first.manifest["selected_counts"] == {
|
||||
"agree": 1,
|
||||
"camera-only": 1,
|
||||
"conflict": 2,
|
||||
"geometry-only": 1,
|
||||
"unknown": 1,
|
||||
}
|
||||
lines = (first.result_root / "review-items.jsonl").read_text().splitlines()
|
||||
items = [json.loads(line) for line in lines]
|
||||
assert len(items) == 6
|
||||
assert all(item["review"]["state"] == "unreviewed" for item in items)
|
||||
assert all(item["materialization"]["source_reprojection_required"] for item in items)
|
||||
assert len({item["item_id"] for item in items}) == 6
|
||||
|
||||
|
||||
def test_review_pack_taxonomy_is_fixed_and_source_tampering_rejects(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
assert E30_REASON_TAXONOMY[0] == "no_lidar_observation"
|
||||
assert E30_REASON_TAXONOMY[-1] == "unknown"
|
||||
assert len(E30_REASON_TAXONOMY) == 19
|
||||
source = _source_result(tmp_path)
|
||||
frames = source / "camera-geometry-frames.jsonl"
|
||||
frames.write_bytes(frames.read_bytes() + b"\n")
|
||||
|
||||
with pytest.raises(E30ReviewPackError, match="byte length changed"):
|
||||
build_e30_review_pack(
|
||||
e29_result_root=source,
|
||||
output_root=tmp_path / "review-packs",
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.lidar_contract import K1_LIVE_LIDAR_PROFILE
|
||||
from k1link.compute.sensor_representation import (
|
||||
K1_LIO_PCL_CAPABILITIES,
|
||||
SensorAlgorithmRequirements,
|
||||
SensorCapability,
|
||||
SensorRepresentationCapabilities,
|
||||
SensorRepresentationContractError,
|
||||
SensorRepresentationKind,
|
||||
SensorSourceCurrentness,
|
||||
assess_sensor_algorithm,
|
||||
require_sensor_algorithm_admission,
|
||||
)
|
||||
|
||||
|
||||
def _projective_mapper_requirements() -> SensorAlgorithmRequirements:
|
||||
return SensorAlgorithmRequirements(
|
||||
algorithm_id="lidar-projective-mapper/v1",
|
||||
accepted_representations=(SensorRepresentationKind.NATIVE_SENSOR_SCAN,),
|
||||
required_capabilities=frozenset(
|
||||
{
|
||||
SensorCapability.METRIC_XYZ,
|
||||
SensorCapability.NATIVE_RAY_MODEL,
|
||||
SensorCapability.SENSOR_ORIGIN_PER_POINT,
|
||||
SensorCapability.RAY_CLEARING_VALID,
|
||||
SensorCapability.FREE_SPACE_EVIDENCE_VALID,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_k1_lio_pcl_capabilities_round_trip_without_free_space_or_authority() -> None:
|
||||
document = K1_LIO_PCL_CAPABILITIES.to_dict()
|
||||
restored = SensorRepresentationCapabilities.from_dict(document)
|
||||
|
||||
assert restored == K1_LIO_PCL_CAPABILITIES
|
||||
assert restored.source_profile_id == K1_LIVE_LIDAR_PROFILE.profile_id
|
||||
assert document["representation_kind"] == "registered-map-increment"
|
||||
assert document["source_currentness"] == "frame-increment"
|
||||
assert document["semantics"] == {
|
||||
"absence_of_endpoints_means_free": False,
|
||||
"unknown_remains_unknown": True,
|
||||
}
|
||||
assert document["authority"] == {
|
||||
"compatibility_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
assert not restored.supports(SensorCapability.NATIVE_RAY_MODEL)
|
||||
assert not restored.supports(SensorCapability.RAY_CLEARING_VALID)
|
||||
assert not restored.supports(SensorCapability.FREE_SPACE_EVIDENCE_VALID)
|
||||
|
||||
|
||||
def test_k1_lio_pcl_rejects_projective_free_space_mapper() -> None:
|
||||
requirements = _projective_mapper_requirements()
|
||||
|
||||
admission = assess_sensor_algorithm(K1_LIO_PCL_CAPABILITIES, requirements)
|
||||
|
||||
assert admission.admitted is False
|
||||
assert admission.reasons == (
|
||||
"representation-not-accepted:registered-map-increment",
|
||||
"missing-capability:free_space_evidence_valid",
|
||||
"missing-capability:native_ray_model",
|
||||
"missing-capability:ray_clearing_valid",
|
||||
"missing-capability:sensor_origin_per_point",
|
||||
)
|
||||
with pytest.raises(
|
||||
SensorRepresentationContractError,
|
||||
match="lidar-projective-mapper/v1 rejected",
|
||||
):
|
||||
require_sensor_algorithm_admission(K1_LIO_PCL_CAPABILITIES, requirements)
|
||||
|
||||
|
||||
def test_k1_lio_pcl_admits_endpoint_marking_without_granting_authority() -> None:
|
||||
requirements = SensorAlgorithmRequirements(
|
||||
algorithm_id="endpoint-occupied-marking/v1",
|
||||
accepted_representations=(
|
||||
SensorRepresentationKind.REGISTERED_MAP_INCREMENT,
|
||||
),
|
||||
required_capabilities=frozenset(
|
||||
{
|
||||
SensorCapability.METRIC_XYZ,
|
||||
SensorCapability.MAP_REGISTERED,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
admission = require_sensor_algorithm_admission(
|
||||
K1_LIO_PCL_CAPABILITIES,
|
||||
requirements,
|
||||
)
|
||||
|
||||
assert admission.admitted is True
|
||||
assert admission.reasons == ()
|
||||
assert admission.to_dict()["authority"] == {
|
||||
"compatibility_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def test_complete_native_scan_admits_projective_mapper() -> None:
|
||||
profile = SensorRepresentationCapabilities(
|
||||
profile_id="synthetic-native-lidar-capabilities/v1",
|
||||
source_profile_id="synthetic-native-lidar/v1",
|
||||
representation_kind=SensorRepresentationKind.NATIVE_SENSOR_SCAN,
|
||||
coordinate_frame="lidar",
|
||||
source_currentness=SensorSourceCurrentness.CURRENT_OBSERVATION,
|
||||
capabilities=frozenset(
|
||||
{
|
||||
SensorCapability.METRIC_XYZ,
|
||||
SensorCapability.METRIC_INTENSITY,
|
||||
SensorCapability.SENSOR_POSE_AVAILABLE,
|
||||
SensorCapability.PER_POINT_TIME,
|
||||
SensorCapability.RING_OR_CHANNEL,
|
||||
SensorCapability.SEPARATE_IMU,
|
||||
SensorCapability.SHARED_HARDWARE_CLOCK,
|
||||
SensorCapability.NATIVE_RAY_MODEL,
|
||||
SensorCapability.SENSOR_ORIGIN_PER_POINT,
|
||||
SensorCapability.RAY_CLEARING_VALID,
|
||||
SensorCapability.MOTION_COMPENSATION_VALID,
|
||||
SensorCapability.FREE_SPACE_EVIDENCE_VALID,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert require_sensor_algorithm_admission(
|
||||
profile,
|
||||
_projective_mapper_requirements(),
|
||||
).admitted
|
||||
|
||||
|
||||
def test_contract_rejects_invented_free_space_and_authority() -> None:
|
||||
with pytest.raises(
|
||||
SensorRepresentationContractError,
|
||||
match="free-space evidence requires admitted ray clearing",
|
||||
):
|
||||
replace(
|
||||
K1_LIO_PCL_CAPABILITIES,
|
||||
capabilities=K1_LIO_PCL_CAPABILITIES.capabilities
|
||||
| {SensorCapability.FREE_SPACE_EVIDENCE_VALID},
|
||||
)
|
||||
|
||||
document = K1_LIO_PCL_CAPABILITIES.to_dict()
|
||||
authority = document["authority"]
|
||||
assert isinstance(authority, dict)
|
||||
authority["navigation_or_safety_accepted"] = True
|
||||
with pytest.raises(
|
||||
SensorRepresentationContractError,
|
||||
match="cannot grant authority",
|
||||
):
|
||||
SensorRepresentationCapabilities.from_dict(document)
|
||||
|
||||
|
||||
def test_algorithm_requirements_are_strict_and_round_trip() -> None:
|
||||
requirements = _projective_mapper_requirements()
|
||||
restored = SensorAlgorithmRequirements.from_dict(requirements.to_dict())
|
||||
|
||||
assert restored == requirements
|
||||
document = requirements.to_dict()
|
||||
document["on_capability_mismatch"] = "degrade"
|
||||
with pytest.raises(
|
||||
SensorRepresentationContractError,
|
||||
match="mismatch must reject",
|
||||
):
|
||||
SensorAlgorithmRequirements.from_dict(document)
|
||||
Reference in New Issue
Block a user