feat(lab): publish M4.8 assisted regression evidence
This commit is contained in:
@@ -9,7 +9,11 @@ from fastapi.routing import APIRoute
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
import k1link.web.advanced_laboratory_api as advanced_api
|
||||
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory import (
|
||||
LaboratoryEvidenceDefinition,
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryEvidenceVariant,
|
||||
)
|
||||
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
||||
|
||||
|
||||
@@ -74,6 +78,79 @@ def test_advanced_index_is_empty_when_not_configured() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path) -> None:
|
||||
variants = (
|
||||
LaboratoryEvidenceVariant(
|
||||
phase="review",
|
||||
runtime_relative_root=PurePosixPath("packs"),
|
||||
result_id_prefix="quality-pack",
|
||||
document_name="manifest.json",
|
||||
result_schema_version="missioncore.quality-pack/v1",
|
||||
),
|
||||
LaboratoryEvidenceVariant(
|
||||
phase="result",
|
||||
runtime_relative_root=PurePosixPath("results"),
|
||||
result_id_prefix="quality-result",
|
||||
document_name="manifest.json",
|
||||
result_schema_version="missioncore.quality-result/v1",
|
||||
),
|
||||
)
|
||||
registry = LaboratoryEvidenceRegistry(
|
||||
definitions=(
|
||||
LaboratoryEvidenceDefinition(
|
||||
work_id="quality-lab",
|
||||
runtime_relative_root=variants[-1].runtime_relative_root,
|
||||
result_id_prefix=variants[-1].result_id_prefix,
|
||||
document_name=variants[-1].document_name,
|
||||
result_schema_version=variants[-1].result_schema_version,
|
||||
lifecycle_variants=variants,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def publish(variant: LaboratoryEvidenceVariant, digest: str, created_at: str) -> str:
|
||||
result_id = f"{variant.result_id_prefix}-{digest}"
|
||||
result_root = variant.result_root(tmp_path) / result_id
|
||||
result_root.mkdir(parents=True)
|
||||
(result_root / variant.document_name).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": variant.result_schema_version,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": digest,
|
||||
"identity": {
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
},
|
||||
"created_at_utc": created_at,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result_id
|
||||
|
||||
pack_id = publish(variants[0], "a" * 64, "2026-08-24T10:00:00Z")
|
||||
router = build_advanced_laboratory_router(
|
||||
evidence_registry=registry,
|
||||
evidence_runtime_root_provider=lambda: tmp_path,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
assert route()["items"][0]["result_id"] == pack_id # type: ignore[index,operator]
|
||||
|
||||
result_id = publish(variants[1], "b" * 64, "2026-08-24T11:00:00Z")
|
||||
index = route() # type: ignore[operator]
|
||||
assert index["items"] == [ # type: ignore[index]
|
||||
{
|
||||
"work_id": "quality-lab",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": "2026-08-24T11:00:00Z",
|
||||
"access": "read-only",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_advanced_index_includes_valid_l31_identity(
|
||||
tmp_path: Path,
|
||||
monkeypatch: MonkeyPatch,
|
||||
|
||||
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
||||
repository_root / "config" / "laboratories"
|
||||
)
|
||||
|
||||
assert len(registry.definitions) == 34
|
||||
assert len(registry.definitions) == 36
|
||||
assert {item.work_id for item in registry.definitions} >= {
|
||||
"e31-source-binding",
|
||||
"e46j-raw-fisheye-realtime",
|
||||
@@ -139,4 +139,15 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
||||
"l34f-adjudicated-reference",
|
||||
"m4-replay-threat",
|
||||
"m47-reference-graph-shadow",
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
}
|
||||
m48 = next(
|
||||
item for item in registry.definitions
|
||||
if item.work_id == "m48-object-centric-quality"
|
||||
)
|
||||
assert [variant.phase for variant in m48.evidence_variants] == ["review", "result"]
|
||||
assert [variant.result_id_prefix for variant in m48.evidence_variants] == [
|
||||
"m48-object-quality-pack",
|
||||
"m48-object-quality-result",
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ import hashlib
|
||||
import json
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -90,6 +91,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
||||
evidence, execution = _registries()
|
||||
|
||||
assert {row.work_id for row in execution.definitions} == {
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-object-centric-quality",
|
||||
"m4-replay-threat",
|
||||
"e33-worker-shadow",
|
||||
"e35-degradation-recovery",
|
||||
@@ -97,6 +100,12 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
||||
"e47-semantic-slam-shadow",
|
||||
}
|
||||
by_work_id = {row.work_id: row for row in execution.definitions}
|
||||
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
|
||||
"missioncore.m48-small-static-passage-regression-result/v1"
|
||||
)
|
||||
assert by_work_id["m48-object-centric-quality"].evidence_contract == (
|
||||
"missioncore.m48-object-centric-quality-result/v1"
|
||||
)
|
||||
assert by_work_id["e47-semantic-slam-shadow"].lifecycle == "experimental"
|
||||
assert by_work_id["e47-semantic-slam-shadow"].isolation == "bounded-adapter"
|
||||
assert all(
|
||||
@@ -193,6 +202,65 @@ def test_runner_rejects_undeclared_input_before_adapter(tmp_path: Path) -> None:
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_m48_evaluation_uses_registered_adapter_and_common_receipt(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
evidence, execution = _registries()
|
||||
pack_root = tmp_path / "pack"
|
||||
truth_root = tmp_path / "truth"
|
||||
pack_root.mkdir()
|
||||
truth_root.mkdir()
|
||||
adapter_result = _evidence_result(
|
||||
tmp_path / "results",
|
||||
work_id="m48-object-centric-quality",
|
||||
)
|
||||
|
||||
def score(**kwargs: Path) -> SimpleNamespace:
|
||||
assert kwargs == {
|
||||
"pack_root": pack_root,
|
||||
"truth_seal_root": truth_root,
|
||||
"output_root": tmp_path / "results",
|
||||
}
|
||||
return SimpleNamespace(
|
||||
result_root=adapter_result.result_root,
|
||||
result_id=adapter_result.result_id,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"k1link.laboratory.m48_object_quality.score_m48_object_quality",
|
||||
score,
|
||||
)
|
||||
runner = LaboratoryRunner(
|
||||
registry=execution,
|
||||
evidence_registry=evidence,
|
||||
sink=JsonlPipelineTelemetrySink(tmp_path / "pipeline.jsonl"),
|
||||
)
|
||||
request = LaboratoryRunRequest(
|
||||
work_id="m48-object-centric-quality",
|
||||
run_id="m48-evaluation-fixture",
|
||||
request_id="evaluate-once",
|
||||
contour_id="mission-core-lab",
|
||||
agent_id="local-control-plane",
|
||||
node_id="fixture-node",
|
||||
source_id="m48-pack-fixture",
|
||||
source_package_id="m48-truth-fixture",
|
||||
method_id="m48-object-centric-quality/v1",
|
||||
inputs={"pack_root": pack_root, "truth_seal_root": truth_root},
|
||||
output_root=tmp_path / "results",
|
||||
receipt_root=tmp_path / "receipts",
|
||||
)
|
||||
|
||||
result = runner.run(request)
|
||||
|
||||
assert result.result_id == adapter_result.result_id
|
||||
assert result.receipt["adapter_id"] == "canonical.m48-object-centric-quality/v1"
|
||||
assert result.receipt["contracts"]["evidence"] == (
|
||||
"missioncore.m48-object-centric-quality-result/v1"
|
||||
)
|
||||
assert (result.receipt_root / "receipt.json").is_file()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.laboratory.m48_object_quality as m48
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n")
|
||||
|
||||
|
||||
def _frame_catalog() -> list[dict[str, object]]:
|
||||
return [
|
||||
{
|
||||
"sequence": sequence,
|
||||
"source_time_ns": (sequence - 1) * 100_000_000,
|
||||
"camera_fragment_sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
|
||||
}
|
||||
for sequence in range(1, 4490)
|
||||
]
|
||||
|
||||
|
||||
def _clips() -> list[dict[str, object]]:
|
||||
rows = []
|
||||
for index in range(20):
|
||||
start = 1 + index * 100
|
||||
split = "development" if index < 10 else "validation"
|
||||
split_index = index if index < 10 else index - 10
|
||||
rows.append(
|
||||
{
|
||||
"clip_id": f"clip-{index:02d}",
|
||||
"component_id": f"component-{split}-{split_index // 2:02d}",
|
||||
"route_block": f"route-{split}-{split_index // 3:02d}",
|
||||
"time_block": f"time-{split}-{split_index // 2:02d}",
|
||||
"split": split,
|
||||
"start_sequence": start,
|
||||
"end_sequence": start + 50,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _clip_fixture_state(clip_id: str) -> dict[str, object]:
|
||||
local_index = int(clip_id.rsplit("-", 1)[1]) % 10
|
||||
if local_index == 0:
|
||||
return {
|
||||
"extent_xyxy": [0.2, 0.2, 0.22, 0.22],
|
||||
"geometry_association": "unknown",
|
||||
"motion": "unsupported",
|
||||
"threat": "unknown",
|
||||
"unknown_causes": [
|
||||
"insufficient-geometry-support",
|
||||
"threat-evidence-insufficient",
|
||||
],
|
||||
}
|
||||
if local_index == 1:
|
||||
return {
|
||||
"extent_xyxy": [0.01, 0.2, 0.2, 0.4],
|
||||
"geometry_association": "associated",
|
||||
"motion": "static",
|
||||
"threat": "not-threat",
|
||||
"unknown_causes": [],
|
||||
}
|
||||
if local_index == 2:
|
||||
return {
|
||||
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
|
||||
"geometry_association": "associated",
|
||||
"motion": "moving",
|
||||
"threat": "threat",
|
||||
"unknown_causes": [],
|
||||
}
|
||||
return {
|
||||
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
|
||||
"geometry_association": "associated",
|
||||
"motion": "static",
|
||||
"threat": "not-threat",
|
||||
"unknown_causes": [],
|
||||
}
|
||||
|
||||
|
||||
def _preparation_provenance() -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": m48.M48_PREPARATION_PROVENANCE_SCHEMA,
|
||||
"adapter": {
|
||||
"module": "k1link.laboratory.m48_ravnoves00_pack",
|
||||
"sha256": "1" * 64,
|
||||
},
|
||||
"selection": {
|
||||
"selection_id": "m48-ravnoves00-balanced-connected-clips/v1",
|
||||
"sha256": "2" * 64,
|
||||
},
|
||||
"camera_index": {
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"sha256": "3" * 64,
|
||||
"byte_length": 1234,
|
||||
"frame_count": 4489,
|
||||
},
|
||||
"graph": {
|
||||
"result_id": "m47-reference-graph-" + "4" * 64,
|
||||
"manifest_sha256": "5" * 64,
|
||||
"frames_sha256": "6" * 64,
|
||||
},
|
||||
"threat": {
|
||||
"result_id": "m4-threat-replay-" + "7" * 64,
|
||||
"manifest_sha256": "8" * 64,
|
||||
"frames_sha256": "9" * 64,
|
||||
},
|
||||
"geometry": {
|
||||
"result_id": "m4-geometry-replay-" + "a" * 64,
|
||||
"manifest_sha256": "b" * 64,
|
||||
"frames_sha256": "c" * 64,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _prediction_rows(
|
||||
clips: list[dict[str, object]],
|
||||
*,
|
||||
unsafe_free_space: bool,
|
||||
unsafe_free_space_split: str | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
for clip in clips:
|
||||
fixture = _clip_fixture_state(str(clip["clip_id"]))
|
||||
unsafe = unsafe_free_space and (
|
||||
unsafe_free_space_split is None or clip["split"] == unsafe_free_space_split
|
||||
)
|
||||
for sequence in range(int(clip["start_sequence"]), int(clip["end_sequence"]) + 1):
|
||||
objects = [
|
||||
{
|
||||
"prediction_id": f"prediction-{sequence}",
|
||||
"extent_xyxy": fixture["extent_xyxy"],
|
||||
"geometry_association": fixture["geometry_association"],
|
||||
"freshness": "current",
|
||||
"motion": fixture["motion"],
|
||||
"threat": fixture["threat"],
|
||||
"unknown_causes": fixture["unknown_causes"],
|
||||
}
|
||||
]
|
||||
rows.append(
|
||||
{
|
||||
"sequence": sequence,
|
||||
"source_time_ns": (sequence - 1) * 100_000_000,
|
||||
"terminal_outcome": "delivered",
|
||||
"terminal_reason": None,
|
||||
"free_space_claimed": unsafe,
|
||||
"objects": objects,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _fake_m47(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
root = tmp_path / f"m47-reference-graph-lab-{'a' * 64}"
|
||||
root.mkdir()
|
||||
manifest = {
|
||||
"schema_version": "missioncore.reference-perception-graph-lab/v2",
|
||||
"accepted": True,
|
||||
"ground_truth": False,
|
||||
}
|
||||
_write_json(root / "manifest.json", manifest)
|
||||
report = {
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"graph_result_id": "m47-reference-graph-" + "b" * 64,
|
||||
},
|
||||
"method": {
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"run_mode": "lossless-replay",
|
||||
"canonical_payload_sha256": "c" * 64,
|
||||
},
|
||||
"decision": {
|
||||
"state": "accepted-reference-graph-replay",
|
||||
"next_gate": "independent-object-centric-detection-quality",
|
||||
},
|
||||
"acceptance": {"accepted": True},
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"ground_truth": False,
|
||||
},
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
m48,
|
||||
"read_m47_reference_graph_lab",
|
||||
lambda _: SimpleNamespace(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _review_clips(clips: tuple[dict[str, Any], ...], *, state: str) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for clip in clips:
|
||||
fixture = _clip_fixture_state(str(clip["clip_id"]))
|
||||
start = int(clip["start_sequence"])
|
||||
end = int(clip["end_sequence"])
|
||||
rows.append(
|
||||
{
|
||||
"clip_id": clip["clip_id"],
|
||||
"start_sequence": start,
|
||||
"end_sequence": end,
|
||||
"review_state": state,
|
||||
"no_object": False,
|
||||
"tracklets": [
|
||||
{
|
||||
"object_id": "object-1",
|
||||
"first_sequence": start,
|
||||
"last_sequence": end,
|
||||
"keyframes": [
|
||||
{
|
||||
"sequence": start,
|
||||
"extent_xyxy": fixture["extent_xyxy"],
|
||||
"visibility": "visible",
|
||||
},
|
||||
{
|
||||
"sequence": end,
|
||||
"extent_xyxy": fixture["extent_xyxy"],
|
||||
"visibility": "partial",
|
||||
},
|
||||
],
|
||||
"state_segments": [
|
||||
{
|
||||
"start_sequence": start,
|
||||
"end_sequence": end,
|
||||
"geometry_association": fixture["geometry_association"],
|
||||
"freshness": "current",
|
||||
"motion": fixture["motion"],
|
||||
"threat": fixture["threat"],
|
||||
"critical_corridor_obstacle": True,
|
||||
}
|
||||
],
|
||||
"notes": None,
|
||||
}
|
||||
],
|
||||
"notes": None,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _review(pack: m48.M48ObjectQualityPack, reviewer_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": m48.M48_REVIEW_SCHEMA,
|
||||
"pack_id": pack.result_id,
|
||||
"state": "completed-independent-no-predictions",
|
||||
"reviewer_id": reviewer_id,
|
||||
"review_round": 1,
|
||||
"blindness": {
|
||||
"candidate_identity_seen": False,
|
||||
"model_predictions_seen": False,
|
||||
"model_scores_seen": False,
|
||||
"semantic_class_task_seen": False,
|
||||
},
|
||||
"clips": _review_clips(pack.clips, state="reviewed"),
|
||||
"acceptance": {
|
||||
"all_clips_reviewed": True,
|
||||
"independent": True,
|
||||
"submitted_at_utc": "2026-08-24T11:00:00Z",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_generations(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
*,
|
||||
unsafe_free_space: bool = False,
|
||||
unsafe_free_space_split: str | None = None,
|
||||
) -> tuple[m48.M48ObjectQualityPack, m48.M48ObjectTruthSeal]:
|
||||
_fake_m47(tmp_path, monkeypatch)
|
||||
clips = _clips()
|
||||
pack = m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=_prediction_rows(
|
||||
clips,
|
||||
unsafe_free_space=unsafe_free_space,
|
||||
unsafe_free_space_split=unsafe_free_space_split,
|
||||
),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs",
|
||||
)
|
||||
review_a = _review(pack, "reviewer-a")
|
||||
review_b = _review(pack, "reviewer-b")
|
||||
review_a_path = tmp_path / "review-a.json"
|
||||
review_b_path = tmp_path / "review-b.json"
|
||||
_write_json(review_a_path, review_a)
|
||||
_write_json(review_b_path, review_b)
|
||||
adjudication = {
|
||||
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
|
||||
"pack_id": pack.result_id,
|
||||
"state": "completed-adjudicated",
|
||||
"adjudicator_id": "adjudicator-1",
|
||||
"review_submission_sha256": sorted(
|
||||
(m48._canonical_sha256(review_a), m48._canonical_sha256(review_b))
|
||||
),
|
||||
"clips": _review_clips(pack.clips, state="adjudicated"),
|
||||
"acceptance": {
|
||||
"all_clips_adjudicated": True,
|
||||
"all_disagreements_resolved": True,
|
||||
"sealed_at_utc": "2026-08-24T12:00:00Z",
|
||||
},
|
||||
}
|
||||
adjudication_path = tmp_path / "adjudication.json"
|
||||
_write_json(adjudication_path, adjudication)
|
||||
truth = m48.build_m48_object_truth_seal(
|
||||
pack_root=pack.result_root,
|
||||
reviewer_a_path=review_a_path,
|
||||
reviewer_b_path=review_b_path,
|
||||
adjudication_path=adjudication_path,
|
||||
output_root=tmp_path / "truth",
|
||||
)
|
||||
return pack, truth
|
||||
|
||||
|
||||
def test_m48_pack_is_neutral_tracklet_review_evidence(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack, truth = _build_generations(tmp_path, monkeypatch)
|
||||
|
||||
reviewer_package = json.loads(
|
||||
(pack.result_root / "reviewer-package.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert reviewer_package["strata_included"] is False
|
||||
assert reviewer_package["frozen_predictions_included"] is False
|
||||
assert all(
|
||||
"strata" not in clip and "selection_hypotheses" not in clip
|
||||
for clip in reviewer_package["clips"]
|
||||
)
|
||||
assert {
|
||||
hypothesis
|
||||
for clip in pack.clips
|
||||
if clip["split"] == "validation"
|
||||
for hypothesis in clip["selection_hypotheses"]
|
||||
} == set(pack.manifest["identity"]["profile"]["required_validation_hypotheses"])
|
||||
review_template = json.loads(
|
||||
(pack.result_root / "review-template.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert "clips" in review_template and "frames" not in review_template
|
||||
assert "tracklets" in review_template["clips"][0]
|
||||
assert len(truth.truth_rows) == len(pack.frame_references)
|
||||
assert truth.truth_rows[0]["objects"][0]["visibility"] == "visible"
|
||||
assert truth.truth_rows[50]["objects"][0]["visibility"] == "partial"
|
||||
|
||||
|
||||
def test_m48_perfect_class_free_result_passes_all_gates_and_registry(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack, truth = _build_generations(tmp_path, monkeypatch)
|
||||
result = m48.score_m48_object_quality(
|
||||
pack_root=pack.result_root,
|
||||
truth_seal_root=truth.result_root,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
assert result.report["acceptance"]["accepted"] is True
|
||||
assert all(result.report["acceptance"]["gates"].values())
|
||||
assert result.report["acceptance"]["scope"] == "validation-only"
|
||||
assert result.report["metrics"] == result.report["metrics_by_split"]["validation"]
|
||||
assert result.report["method"]["semantic_class_scored"] is False
|
||||
assert result.report["decision"]["next_gate"] == ("m4.9-recorded-realtime-release-candidate")
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
registry = LaboratoryEvidenceRegistry.from_directory(repository_root / "config/laboratories")
|
||||
definitions = {definition.work_id: definition for definition in registry.definitions}
|
||||
pack_proof = verify_laboratory_evidence_result(
|
||||
definitions["m48-object-centric-quality"], pack.result_root
|
||||
)
|
||||
result_proof = verify_laboratory_evidence_result(
|
||||
definitions["m48-object-centric-quality"], result.result_root
|
||||
)
|
||||
assert pack_proof["artifact_count"] == 7
|
||||
assert result_proof["artifact_count"] == 3
|
||||
|
||||
|
||||
def test_m48_review_rejects_semantic_class_and_same_reviewer(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_fake_m47(tmp_path, monkeypatch)
|
||||
clips = _clips()
|
||||
pack = m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=_prediction_rows(clips, unsafe_free_space=False),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs",
|
||||
)
|
||||
review_a = _review(pack, "reviewer-a")
|
||||
review_a["clips"][0]["tracklets"][0]["category"] = "car"
|
||||
review_a_path = tmp_path / "review-a.json"
|
||||
_write_json(review_a_path, review_a)
|
||||
with pytest.raises(m48.M48ObjectQualityError, match="fields"):
|
||||
m48.validate_m48_review_submission(pack_root=pack.result_root, review_path=review_a_path)
|
||||
|
||||
review_a = _review(pack, "reviewer-a")
|
||||
review_b = copy.deepcopy(review_a)
|
||||
review_a_path = tmp_path / "review-a-clean.json"
|
||||
review_b_path = tmp_path / "review-b-same.json"
|
||||
_write_json(review_a_path, review_a)
|
||||
_write_json(review_b_path, review_b)
|
||||
adjudication = {
|
||||
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
|
||||
"pack_id": pack.result_id,
|
||||
"state": "completed-adjudicated",
|
||||
"adjudicator_id": "adjudicator-1",
|
||||
"review_submission_sha256": [m48._canonical_sha256(review_a)] * 2,
|
||||
"clips": _review_clips(pack.clips, state="adjudicated"),
|
||||
"acceptance": {
|
||||
"all_clips_adjudicated": True,
|
||||
"all_disagreements_resolved": True,
|
||||
"sealed_at_utc": "2026-08-24T12:00:00Z",
|
||||
},
|
||||
}
|
||||
adjudication_path = tmp_path / "adjudication.json"
|
||||
_write_json(adjudication_path, adjudication)
|
||||
with pytest.raises(m48.M48ObjectQualityError, match="must differ"):
|
||||
m48.build_m48_object_truth_seal(
|
||||
pack_root=pack.result_root,
|
||||
reviewer_a_path=review_a_path,
|
||||
reviewer_b_path=review_b_path,
|
||||
adjudication_path=adjudication_path,
|
||||
output_root=tmp_path / "truth",
|
||||
)
|
||||
|
||||
|
||||
def test_m48_unsafe_free_space_fails_with_bounded_atlas(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack, truth = _build_generations(tmp_path, monkeypatch, unsafe_free_space=True)
|
||||
result = m48.score_m48_object_quality(
|
||||
pack_root=pack.result_root,
|
||||
truth_seal_root=truth.result_root,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
assert result.report["acceptance"]["accepted"] is False
|
||||
assert result.report["acceptance"]["gates"]["false_free_space_claims"] is False
|
||||
assert result.report["acceptance"]["gates"]["critical_corridor_obstacle_recall"] is True
|
||||
assert result.failure_atlas
|
||||
assert any("false-free-space-claim" in row["causes"] for row in result.failure_atlas)
|
||||
assert result.report["decision"]["next_gate"] == (
|
||||
"bounded-cause-remediation-on-failed-m48-clusters"
|
||||
)
|
||||
|
||||
|
||||
def test_m48_development_failures_are_reported_but_cannot_fail_release(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack, truth = _build_generations(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
unsafe_free_space=True,
|
||||
unsafe_free_space_split="development",
|
||||
)
|
||||
result = m48.score_m48_object_quality(
|
||||
pack_root=pack.result_root,
|
||||
truth_seal_root=truth.result_root,
|
||||
output_root=tmp_path / "results",
|
||||
)
|
||||
|
||||
assert result.report["acceptance"]["accepted"] is True
|
||||
assert result.report["metrics_by_split"]["development"]["false_free_space_claims"] > 0
|
||||
assert result.report["metrics"]["false_free_space_claims"] == 0
|
||||
assert all(result.report["acceptance"]["gates"].values())
|
||||
assert any(row["split"] == "development" for row in result.failure_atlas)
|
||||
|
||||
|
||||
def test_m48_rejects_incomplete_clip_contract(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_fake_m47(tmp_path, monkeypatch)
|
||||
clips = _clips()[:19]
|
||||
with pytest.raises(m48.M48ObjectQualityError, match="20–30"):
|
||||
m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=_prediction_rows(clips, unsafe_free_space=False),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs",
|
||||
)
|
||||
|
||||
|
||||
def test_m48_rejects_cross_split_and_vacuous_grouping(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_fake_m47(tmp_path, monkeypatch)
|
||||
clips = _clips()
|
||||
clips[10]["route_block"] = clips[0]["route_block"]
|
||||
with pytest.raises(m48.M48ObjectQualityError, match="route_block crosses"):
|
||||
m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=_prediction_rows(clips, unsafe_free_space=False),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs-cross-split",
|
||||
)
|
||||
|
||||
clips = _clips()
|
||||
for index, clip in enumerate(clips):
|
||||
clip["component_id"] = f"unique-component-{index:02d}"
|
||||
with pytest.raises(m48.M48ObjectQualityError, match="non-vacuous"):
|
||||
m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=_prediction_rows(clips, unsafe_free_space=False),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs-vacuous",
|
||||
)
|
||||
|
||||
|
||||
def test_m48_profile_config_matches_executable_contract() -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
document = json.loads(
|
||||
(repository_root / "config/perception/m48-object-quality-v1.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
profile = m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE
|
||||
|
||||
assert document["schema_version"] == m48.M48_PROFILE_SCHEMA
|
||||
assert document["profile_id"] == profile.profile_id
|
||||
assert document["clip_contract"]["minimum_clip_count"] == profile.minimum_clip_count
|
||||
assert document["clip_contract"]["maximum_clip_count"] == profile.maximum_clip_count
|
||||
assert document["review_contract"]["review_unit"] == "clip-local-object-tracklet"
|
||||
assert document["review_contract"]["semantic_class_labels_allowed"] is False
|
||||
assert document["review_contract"]["selection_hypotheses_visible_to_reviewers"] is False
|
||||
assert document["clip_contract"]["release_gate_split"] == "validation"
|
||||
assert document["clip_contract"]["required_validation_hypotheses"] == sorted(
|
||||
m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE.to_dict()["required_validation_hypotheses"]
|
||||
)
|
||||
assert document["release_thresholds"]["obstacle_presence_precision"] == (
|
||||
profile.obstacle_presence_precision
|
||||
)
|
||||
assert document["release_thresholds"]["critical_corridor_obstacle_recall"] == (
|
||||
profile.critical_corridor_obstacle_recall
|
||||
)
|
||||
|
||||
|
||||
def test_m48_pack_identity_is_stable_across_clip_and_prediction_input_order(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_fake_m47(tmp_path, monkeypatch)
|
||||
clips = _clips()
|
||||
predictions = _prediction_rows(clips, unsafe_free_space=False)
|
||||
first = m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=predictions,
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs-a",
|
||||
)
|
||||
second = m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=reversed(clips),
|
||||
predictions=reversed(predictions),
|
||||
preparation_provenance=_preparation_provenance(),
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs-b",
|
||||
)
|
||||
|
||||
assert first.result_id == second.result_id
|
||||
assert first.manifest["identity_sha256"] == second.manifest["identity_sha256"]
|
||||
|
||||
changed_provenance = _preparation_provenance()
|
||||
changed_provenance["camera_index"]["sha256"] = "d" * 64
|
||||
third = m48.build_m48_object_quality_pack(
|
||||
m47_lab_root=tmp_path / "ignored-m47",
|
||||
frame_catalog=_frame_catalog(),
|
||||
clips=clips,
|
||||
predictions=predictions,
|
||||
preparation_provenance=changed_provenance,
|
||||
frozen_at_utc="2026-08-24T10:00:00Z",
|
||||
output_root=tmp_path / "packs-c",
|
||||
)
|
||||
assert third.result_id != first.result_id
|
||||
assert third.manifest["identity"]["preparation"]["camera_index"]["sha256"] == ("d" * 64)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from itertools import pairwise
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from k1link.laboratory.m47_reference_graph import M47_REFERENCE_GRAPH_LAB_SCHEMA
|
||||
from k1link.laboratory.m48_object_quality import read_m48_object_quality_pack
|
||||
from k1link.laboratory.m48_ravnoves00_pack import (
|
||||
M48_FRAME_COUNT,
|
||||
M48_SELECTION_SCHEMA,
|
||||
_prediction_objects,
|
||||
prepare_m48_ravnoves00_pack,
|
||||
)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> str:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_text(_canonical_json(value) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: list[dict[str, object]]) -> str:
|
||||
raw = "".join(_canonical_json(row) + "\n" for row in rows).encode()
|
||||
path.write_bytes(raw)
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _recursive_keys(value: object) -> set[str]:
|
||||
if isinstance(value, dict):
|
||||
return set(value) | {key for item in value.values() for key in _recursive_keys(item)}
|
||||
if isinstance(value, list):
|
||||
return {key for item in value for key in _recursive_keys(item)}
|
||||
return set()
|
||||
|
||||
|
||||
def test_prediction_projection_is_class_free_and_conservative() -> None:
|
||||
rows = _prediction_objects(
|
||||
[
|
||||
{
|
||||
"proposal_id": "proposal-0-1",
|
||||
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
|
||||
"occupied_support": False,
|
||||
"threat_decision": "unknown",
|
||||
"semantic_hint": "person",
|
||||
"objectness": 0.99,
|
||||
},
|
||||
{
|
||||
"proposal_id": "proposal-0-2",
|
||||
"bbox_xyxy": [400.0, 300.0, 720.0, 540.0],
|
||||
"occupied_support": True,
|
||||
"threat_decision": "threat",
|
||||
"semantic_hint": "car",
|
||||
"objectness": 0.98,
|
||||
},
|
||||
],
|
||||
geometry_observations=[
|
||||
{
|
||||
"proposal_ids": ["proposal-0-1"],
|
||||
"currentness": "current",
|
||||
"metric_geometry": None,
|
||||
},
|
||||
{
|
||||
"proposal_ids": ["proposal-0-2"],
|
||||
"currentness": "current",
|
||||
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
|
||||
},
|
||||
],
|
||||
metric_obstacles=[
|
||||
{
|
||||
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
|
||||
"motion": "moving",
|
||||
"assessment": {"decision": "threat"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert rows == [
|
||||
{
|
||||
"prediction_id": "proposal-0-1",
|
||||
"extent_xyxy": [0.1, 0.1, 0.5, 0.5],
|
||||
"geometry_association": "unknown",
|
||||
"freshness": "current",
|
||||
"motion": "unsupported",
|
||||
"threat": "unknown",
|
||||
"unknown_causes": [
|
||||
"insufficient-geometry-support",
|
||||
"threat-evidence-insufficient",
|
||||
],
|
||||
},
|
||||
{
|
||||
"prediction_id": "proposal-0-2",
|
||||
"extent_xyxy": [0.5, 0.5, 0.9, 0.9],
|
||||
"geometry_association": "associated",
|
||||
"freshness": "current",
|
||||
"motion": "moving",
|
||||
"threat": "threat",
|
||||
"unknown_causes": [],
|
||||
},
|
||||
]
|
||||
assert "semantic_hint" not in _canonical_json(rows)
|
||||
assert "objectness" not in _canonical_json(rows)
|
||||
|
||||
|
||||
def test_real_selection_contract_is_balanced_and_prediction_blind() -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
document = json.loads(
|
||||
(repository_root / "config/perception/m48-object-quality-selection-v1.json").read_text()
|
||||
)
|
||||
clips = document["clips"]
|
||||
|
||||
assert document["schema_version"] == M48_SELECTION_SCHEMA
|
||||
assert len(clips) == 24
|
||||
assert {clip["split"] for clip in clips} == {"development", "validation"}
|
||||
assert all("strata" not in clip and "hypotheses" not in clip for clip in clips)
|
||||
assert document["selection_hypothesis_profile"] == {
|
||||
"derivation": "exact-frozen-prediction-rows-before-independent-truth",
|
||||
"small_obstacle_max_normalized_area": 0.001,
|
||||
"fisheye_edge_margin_normalized": 0.08,
|
||||
"sparse_scene_max_median_prediction_count": 2.0,
|
||||
}
|
||||
for field in ("component_id", "route_block", "time_block"):
|
||||
group_splits: dict[str, set[str]] = {}
|
||||
for clip in clips:
|
||||
group_splits.setdefault(clip[field], set()).add(clip["split"])
|
||||
assert all(len(splits) == 1 for splits in group_splits.values())
|
||||
assert len(group_splits) < len(clips)
|
||||
assert all(left["end_sequence"] < right["start_sequence"] for left, right in pairwise(clips))
|
||||
forbidden = {"label", "labels", "truth", "review", "adjudication"}
|
||||
assert forbidden.isdisjoint(document)
|
||||
|
||||
|
||||
def test_prepare_pack_binds_all_source_ledgers_and_freezes_selected_frames(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
graph_root = tmp_path / ("m47-reference-graph-" + "a" * 64)
|
||||
threat_root = tmp_path / ("m4-threat-replay-" + "b" * 64)
|
||||
geometry_root = tmp_path / ("m4-geometry-replay-" + "e" * 64)
|
||||
lab_root = tmp_path / ("m47-reference-graph-lab-" + "c" * 64)
|
||||
graph_root.mkdir()
|
||||
threat_root.mkdir()
|
||||
geometry_root.mkdir()
|
||||
lab_root.mkdir()
|
||||
_write_json(lab_root / "manifest.json", {"fixture": True})
|
||||
|
||||
graph_rows: list[dict[str, object]] = []
|
||||
threat_rows: list[dict[str, object]] = []
|
||||
geometry_rows: list[dict[str, object]] = []
|
||||
camera_rows: list[dict[str, object]] = []
|
||||
for frame_index in range(M48_FRAME_COUNT):
|
||||
source_time_ns = 35_421_857_292 + frame_index * 100_000_000
|
||||
graph_rows.append(
|
||||
{
|
||||
"sequence": frame_index,
|
||||
"obstacle_map": {
|
||||
"schema_version": "missioncore.local-obstacle-map/v1",
|
||||
"frame_id": f"frame-{frame_index:06d}",
|
||||
"free_space_claimed": False,
|
||||
},
|
||||
"threats": [],
|
||||
}
|
||||
)
|
||||
threat_rows.append(
|
||||
{
|
||||
"schema_version": "missioncore.perception-threat-replay-frame/v2",
|
||||
"sequence": frame_index,
|
||||
"frame_id": f"frame-{frame_index:06d}",
|
||||
"source_time_ns": source_time_ns,
|
||||
"source_available": True,
|
||||
"camera_proposals": [
|
||||
{
|
||||
"proposal_id": f"proposal-{frame_index}-0",
|
||||
"bbox_xyxy": [0.0, 0.0, 20.0, 20.0],
|
||||
"occupied_support": True,
|
||||
"threat_decision": "threat" if frame_index % 2 == 0 else "not-threat",
|
||||
},
|
||||
{
|
||||
"proposal_id": f"proposal-{frame_index}-1",
|
||||
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
|
||||
"occupied_support": False,
|
||||
"threat_decision": "unknown",
|
||||
},
|
||||
],
|
||||
"metric_obstacles": [
|
||||
{
|
||||
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
|
||||
"motion": "moving" if frame_index % 2 == 0 else "stationary",
|
||||
"assessment": {
|
||||
"decision": "threat" if frame_index % 2 == 0 else "not-threat"
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
geometry_rows.append(
|
||||
{
|
||||
"schema_version": "missioncore.perception-geometry-replay-frame/v1",
|
||||
"sequence": frame_index,
|
||||
"frame_id": f"frame-{frame_index:06d}",
|
||||
"source_available": True,
|
||||
"observations": [
|
||||
{
|
||||
"proposal_ids": [f"proposal-{frame_index}-0"],
|
||||
"currentness": "current",
|
||||
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
|
||||
},
|
||||
{
|
||||
"proposal_ids": [f"proposal-{frame_index}-1"],
|
||||
"currentness": "current",
|
||||
"metric_geometry": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
camera_rows.append(
|
||||
{
|
||||
"schema_version": "missioncore.camera-recording-index/v1",
|
||||
"sequence": frame_index + 1,
|
||||
"kind": "media",
|
||||
"session_monotonic_ns": frame_index + 1,
|
||||
"sha256": hashlib.sha256(f"camera-{frame_index}".encode()).hexdigest(),
|
||||
}
|
||||
)
|
||||
graph_sha256 = _write_jsonl(graph_root / "frames.jsonl", graph_rows)
|
||||
threat_sha256 = _write_jsonl(threat_root / "frames.jsonl", threat_rows)
|
||||
geometry_sha256 = _write_jsonl(geometry_root / "frames.jsonl", geometry_rows)
|
||||
camera_index = tmp_path / "index.jsonl"
|
||||
_write_jsonl(camera_index, camera_rows)
|
||||
_write_json(
|
||||
graph_root / "manifest.json",
|
||||
{
|
||||
"schema_version": "missioncore.reference-perception-graph-manifest/v1",
|
||||
"result_id": graph_root.name,
|
||||
"accepted": True,
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"run_mode": "lossless-replay",
|
||||
"files": {
|
||||
"frames.jsonl": {
|
||||
"bytes": (graph_root / "frames.jsonl").stat().st_size,
|
||||
"sha256": graph_sha256,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
_write_json(
|
||||
threat_root / "manifest.json",
|
||||
{
|
||||
"schema_version": "missioncore.perception-threat-replay-result/v2",
|
||||
"result_id": threat_root.name,
|
||||
"accepted": True,
|
||||
"identity": {
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"frames_sha256": threat_sha256,
|
||||
"geometry_result_id": geometry_root.name,
|
||||
"geometry_frames_sha256": geometry_sha256,
|
||||
},
|
||||
},
|
||||
)
|
||||
_write_json(
|
||||
geometry_root / "manifest.json",
|
||||
{
|
||||
"schema_version": "missioncore.perception-geometry-replay-result/v1",
|
||||
"identity": {
|
||||
"accepted": True,
|
||||
"source_pack_id": (
|
||||
"e10-lidar-pack-"
|
||||
"576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
||||
),
|
||||
"frames_sha256": geometry_sha256,
|
||||
},
|
||||
},
|
||||
)
|
||||
authority = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
lab = SimpleNamespace(
|
||||
result_id=lab_root.name,
|
||||
result_root=lab_root,
|
||||
manifest={
|
||||
"schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA,
|
||||
"accepted": True,
|
||||
"ground_truth": False,
|
||||
},
|
||||
report={
|
||||
"source": {
|
||||
"graph_result_id": graph_root.name,
|
||||
"visual_result_id": threat_root.name,
|
||||
"threat_frames_sha256": threat_sha256,
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
},
|
||||
"method": {
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"run_mode": "lossless-replay",
|
||||
"canonical_payload_sha256": "d" * 64,
|
||||
},
|
||||
"decision": {
|
||||
"state": "accepted-reference-graph-replay",
|
||||
"next_gate": "independent-object-centric-detection-quality",
|
||||
},
|
||||
"acceptance": {"accepted": True},
|
||||
"authority": authority,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"k1link.laboratory.m48_ravnoves00_pack.read_m47_reference_graph_lab",
|
||||
lambda _: lab,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"k1link.laboratory.m48_object_quality.read_m47_reference_graph_lab",
|
||||
lambda _: lab,
|
||||
)
|
||||
|
||||
result = prepare_m48_ravnoves00_pack(
|
||||
m47_lab_root=lab_root,
|
||||
graph_result_root=graph_root,
|
||||
threat_result_root=threat_root,
|
||||
geometry_result_root=geometry_root,
|
||||
camera_index_path=camera_index,
|
||||
selection_path=(repository_root / "config/perception/m48-object-quality-selection-v1.json"),
|
||||
frozen_at_utc="2026-08-24T00:00:00Z",
|
||||
output_root=tmp_path / "runtime/m48/object-quality-packs",
|
||||
)
|
||||
|
||||
assert read_m48_object_quality_pack(result.result_root) == result
|
||||
assert result.report["metrics"]["clip_count"] == 24
|
||||
assert result.report["metrics"]["frame_count"] == 24 * 61
|
||||
assert len(result.predictions) == 24 * 61
|
||||
assert result.manifest["identity"]["preparation"]["adapter"]["sha256"] == (
|
||||
hashlib.sha256(
|
||||
(repository_root / "src/k1link/laboratory/m48_ravnoves00_pack.py").read_bytes()
|
||||
).hexdigest()
|
||||
)
|
||||
assert result.manifest["identity"]["preparation"]["selection"]["sha256"] == (
|
||||
hashlib.sha256(
|
||||
(
|
||||
repository_root / "config/perception/m48-object-quality-selection-v1.json"
|
||||
).read_bytes()
|
||||
).hexdigest()
|
||||
)
|
||||
reviewer_package = json.loads((result.result_root / "reviewer-package.json").read_text())
|
||||
reviewer_keys = _recursive_keys(reviewer_package)
|
||||
assert "strata" not in reviewer_keys
|
||||
assert "prediction_id" not in reviewer_keys
|
||||
assert "semantic_hint" not in reviewer_keys
|
||||
@@ -0,0 +1,350 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.laboratory import m48_raw_evidence as raw_module
|
||||
from k1link.laboratory.m48_object_quality import M48ObjectQualityPack
|
||||
from k1link.laboratory.m48_raw_evidence import (
|
||||
M48_EXPECTED_FRAME_COUNT,
|
||||
M48_EXPECTED_SESSION_ID,
|
||||
M48_EXPECTED_SOURCE_ID,
|
||||
M48_RAW_SPATIAL_FRAME_SCHEMA,
|
||||
M48RawEvidenceError,
|
||||
M48RawEvidenceReader,
|
||||
)
|
||||
from k1link.perception.geometry import RecordedFrameTemporalBinding
|
||||
from k1link.perception.threat import ReplayBodyFrame, load_replay_threat_profile
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v3.json"
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
return hashlib.sha256(
|
||||
json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def _sealed_e10_pack(tmp_path: Path) -> tuple[Path, str, str]:
|
||||
payload = b"sealed-e10-lidar-pack"
|
||||
artifact_sha256 = hashlib.sha256(payload).hexdigest()
|
||||
identity = {
|
||||
"available_lidar_frames": 3928,
|
||||
"calibration_sha256": "1" * 64,
|
||||
"camera_slot": "camera_1",
|
||||
"e6_profile_sha256": "2" * 64,
|
||||
"e6_result_id": "e6-fixture",
|
||||
"frame_count": M48_EXPECTED_FRAME_COUNT,
|
||||
"input_sha256": "3" * 64,
|
||||
"job_id": "recorded-camera-fixture",
|
||||
"point_count": 5,
|
||||
"producer_sha256": "4" * 64,
|
||||
"projection": {
|
||||
"height": 600,
|
||||
"model": "kb4",
|
||||
"source_coordinates": "k1-map",
|
||||
"target_camera": "sensor.camera.right",
|
||||
"width": 800,
|
||||
},
|
||||
"schema_version": "missioncore.e10-lidar-replay-pack/v1",
|
||||
"semantic_timeline_result_id": "result-fixture",
|
||||
"session_id": M48_EXPECTED_SESSION_ID,
|
||||
"source_end_frame_index": M48_EXPECTED_FRAME_COUNT - 1,
|
||||
"source_id": "sensor.camera.right",
|
||||
"source_start_frame_index": 0,
|
||||
"temporal_binding": "accepted-e6-nearest-host-arrival-best-effort",
|
||||
"temporal_policy": {
|
||||
"binding": "nearest-host-arrival-best-effort",
|
||||
"clock_source": "recorded-host-monotonic-arrival",
|
||||
"maximum_lidar_camera_delta_ms": 100.0,
|
||||
"maximum_pose_point_delta_ms": 100.0,
|
||||
},
|
||||
"timeline_end_seconds": 484.0,
|
||||
"timeline_start_seconds": 35.0,
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
pack_id = f"e10-lidar-pack-{identity_sha256}"
|
||||
root = tmp_path / pack_id
|
||||
root.mkdir()
|
||||
(root / "lidar-pack.npz").write_bytes(payload)
|
||||
manifest = {
|
||||
"artifact": {
|
||||
"byte_length": len(payload),
|
||||
"media_type": "application/x-npz",
|
||||
"path": "lidar-pack.npz",
|
||||
"sha256": artifact_sha256,
|
||||
},
|
||||
"classification": "private-recorded-sensor-replay-input",
|
||||
"created_at_utc": "2026-07-22T06:05:22.515Z",
|
||||
"ground_truth": False,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"pack_id": pack_id,
|
||||
"schema_version": "missioncore.e10-lidar-replay-pack/v1",
|
||||
}
|
||||
(root / "manifest.json").write_text(
|
||||
json.dumps(manifest, sort_keys=True, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root, pack_id, artifact_sha256
|
||||
|
||||
|
||||
def test_e10_pack_validation_binds_identity_path_length_and_sha256(tmp_path: Path) -> None:
|
||||
root, pack_id, artifact_sha256 = _sealed_e10_pack(tmp_path)
|
||||
|
||||
artifact = raw_module._validate_e10_pack(
|
||||
root,
|
||||
expected_pack_id=pack_id,
|
||||
expected_artifact_sha256=artifact_sha256,
|
||||
)
|
||||
|
||||
assert artifact == (root / "lidar-pack.npz").resolve()
|
||||
|
||||
(root / "lidar-pack.npz").write_bytes(b"tampered")
|
||||
with pytest.raises(M48RawEvidenceError, match="artifact content changed"):
|
||||
raw_module._validate_e10_pack(
|
||||
root,
|
||||
expected_pack_id=pack_id,
|
||||
expected_artifact_sha256=artifact_sha256,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutation", "message"),
|
||||
[
|
||||
(lambda manifest: manifest["identity"].update(session_id="other"), "identity changed"),
|
||||
(
|
||||
lambda manifest: manifest["artifact"].update(path="../lidar-pack.npz"),
|
||||
"identity changed",
|
||||
),
|
||||
(lambda manifest: manifest.update(pack_id="e10-lidar-pack-wrong"), "identity changed"),
|
||||
],
|
||||
)
|
||||
def test_e10_pack_validation_rejects_manifest_escape(
|
||||
tmp_path: Path,
|
||||
mutation: object,
|
||||
message: str,
|
||||
) -> None:
|
||||
root, pack_id, artifact_sha256 = _sealed_e10_pack(tmp_path)
|
||||
manifest_path = root / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
assert callable(mutation)
|
||||
mutation(manifest)
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
with pytest.raises(M48RawEvidenceError, match=message):
|
||||
raw_module._validate_e10_pack(
|
||||
root,
|
||||
expected_pack_id=pack_id,
|
||||
expected_artifact_sha256=artifact_sha256,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Store:
|
||||
source_time_ns: int = 2_000_000_000
|
||||
source_available: bool = True
|
||||
|
||||
def temporal_binding_for_index(self, frame_index: int) -> RecordedFrameTemporalBinding:
|
||||
return RecordedFrameTemporalBinding(
|
||||
frame_index=frame_index,
|
||||
source_time_ns=self.source_time_ns,
|
||||
source_available=self.source_available,
|
||||
lidar_camera_delta_ms=1.0 if self.source_available else None,
|
||||
pose_point_delta_ms=1.0 if self.source_available else None,
|
||||
)
|
||||
|
||||
def current_points_for_frame(self, frame_index: int) -> np.ndarray:
|
||||
del frame_index
|
||||
return np.asarray(
|
||||
[
|
||||
[1.0, 0.0, 0.0],
|
||||
[2.0, 0.0, 0.0],
|
||||
[3.0, 0.0, 0.0],
|
||||
[4.0, 0.0, 0.0],
|
||||
[5.0, 0.0, 0.0],
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _BodyFrames:
|
||||
available: bool = True
|
||||
|
||||
def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame | None:
|
||||
if not self.available:
|
||||
return None
|
||||
return ReplayBodyFrame(
|
||||
frame_id=frame_id,
|
||||
origin_map_xyz_m=(1.0, 0.0, 0.0),
|
||||
basis_map_from_body=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
sensor_height_m=1.25,
|
||||
surface_slope_deg=0.0,
|
||||
forward_source="fixture",
|
||||
camera_forward_alignment_deg=0.0,
|
||||
)
|
||||
|
||||
|
||||
class _PredictionTrapPack:
|
||||
result_id = "m48-object-quality-pack-" + "a" * 64
|
||||
result_root = REPOSITORY_ROOT
|
||||
manifest = {
|
||||
"identity": {
|
||||
"source": {
|
||||
"source_id": M48_EXPECTED_SOURCE_ID,
|
||||
"source_session_id": M48_EXPECTED_SESSION_ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
report: dict[str, object] = {}
|
||||
clips: tuple[dict[str, object], ...] = ()
|
||||
frame_references = (
|
||||
{
|
||||
"clip_id": "clip-01",
|
||||
"sequence": 2,
|
||||
"source_time_ns": 2_000_000_000,
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def predictions(self) -> object:
|
||||
raise AssertionError("neutral raw reader opened frozen predictions")
|
||||
|
||||
|
||||
def _reader(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
store: _Store | None = None,
|
||||
body_frames: _BodyFrames | None = None,
|
||||
) -> M48RawEvidenceReader:
|
||||
profile = load_replay_threat_profile(PROFILE_PATH)
|
||||
timeline = SimpleNamespace(
|
||||
store=store or _Store(),
|
||||
body_frames=body_frames or _BodyFrames(),
|
||||
profile=profile,
|
||||
)
|
||||
threat = SimpleNamespace(
|
||||
result_id="m4-threat-replay-fixture",
|
||||
result_root=tmp_path,
|
||||
manifest={"identity": {"frames_sha256": "f" * 64}},
|
||||
)
|
||||
return M48RawEvidenceReader(
|
||||
repository_root=tmp_path,
|
||||
threat_result=threat,
|
||||
timeline=timeline,
|
||||
point_limit=2,
|
||||
)
|
||||
|
||||
|
||||
def test_raw_reader_is_one_based_bounded_body_frame_and_prediction_free(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
|
||||
reader = _reader(tmp_path)
|
||||
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
|
||||
|
||||
frame = reader(pack, 2)
|
||||
|
||||
assert set(frame) == {
|
||||
"schema_version",
|
||||
"pack_id",
|
||||
"clip_id",
|
||||
"sequence",
|
||||
"source_time_ns",
|
||||
"source_available",
|
||||
"body_frame_available",
|
||||
"point_cloud_body_xyz_m",
|
||||
"rig",
|
||||
"corridor",
|
||||
"occupied_voxel_size_m",
|
||||
"candidate_identity_included",
|
||||
"graph_boxes_ids_scores_included",
|
||||
"frozen_predictions_included",
|
||||
"strata_included",
|
||||
"authority",
|
||||
}
|
||||
assert frame["schema_version"] == M48_RAW_SPATIAL_FRAME_SCHEMA
|
||||
assert frame["clip_id"] == "clip-01"
|
||||
assert frame["sequence"] == 2
|
||||
assert frame["source_available"] is True
|
||||
assert frame["body_frame_available"] is True
|
||||
assert frame["point_cloud_body_xyz_m"] == [[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]]
|
||||
assert len(cast(list[object], frame["point_cloud_body_xyz_m"])) <= 2
|
||||
assert frame["candidate_identity_included"] is False
|
||||
assert frame["graph_boxes_ids_scores_included"] is False
|
||||
assert frame["frozen_predictions_included"] is False
|
||||
assert frame["strata_included"] is False
|
||||
assert "metric_obstacles" not in frame
|
||||
assert "camera_proposals" not in frame
|
||||
assert "decision_counts" not in frame
|
||||
assert "body_frame" not in frame
|
||||
|
||||
|
||||
def test_raw_reader_fails_closed_on_clip_or_source_time_escape(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
|
||||
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
|
||||
reader = _reader(tmp_path)
|
||||
|
||||
with pytest.raises(M48RawEvidenceError, match="outside the selected neutral clips"):
|
||||
reader(pack, 1)
|
||||
|
||||
mismatched = _reader(tmp_path, store=_Store(source_time_ns=2_000_000_001))
|
||||
with pytest.raises(M48RawEvidenceError, match="source time escaped"):
|
||||
mismatched(pack, 2)
|
||||
|
||||
|
||||
def test_raw_reader_emits_empty_cloud_when_body_frame_is_unavailable(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
|
||||
reader = _reader(
|
||||
tmp_path,
|
||||
store=_Store(source_available=False),
|
||||
body_frames=_BodyFrames(available=False),
|
||||
)
|
||||
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
|
||||
|
||||
frame = reader.frame(pack=pack, sequence=2)
|
||||
|
||||
assert frame["source_available"] is False
|
||||
assert frame["body_frame_available"] is False
|
||||
assert frame["point_cloud_body_xyz_m"] == []
|
||||
assert isinstance(frame["rig"], dict)
|
||||
assert isinstance(frame["corridor"], dict)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("point_limit", [0, 4097, True])
|
||||
def test_raw_reader_rejects_unbounded_point_limits(
|
||||
tmp_path: Path,
|
||||
point_limit: int,
|
||||
) -> None:
|
||||
profile = load_replay_threat_profile(PROFILE_PATH)
|
||||
timeline = SimpleNamespace(store=_Store(), body_frames=_BodyFrames(), profile=profile)
|
||||
threat = SimpleNamespace(result_id="fixture", result_root=tmp_path, manifest={})
|
||||
|
||||
with pytest.raises(M48RawEvidenceError, match="point limit"):
|
||||
M48RawEvidenceReader(
|
||||
repository_root=tmp_path,
|
||||
threat_result=threat,
|
||||
timeline=timeline,
|
||||
point_limit=point_limit,
|
||||
)
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.laboratory.m48_small_static_regression as regression
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
AUTHORITY = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def _canonical(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical(value) + b"\n")
|
||||
|
||||
|
||||
def _correction(pack_id: str) -> dict[str, object]:
|
||||
def tracklet(object_id: str, extent: list[float], *, passage: bool) -> dict[str, object]:
|
||||
return {
|
||||
"object_id": object_id,
|
||||
"first_sequence": 10,
|
||||
"last_sequence": 10,
|
||||
"keyframes": [{
|
||||
"sequence": 10,
|
||||
"extent_xyxy": extent,
|
||||
"visibility": "visible",
|
||||
}],
|
||||
"state_segments": [{
|
||||
"start_sequence": 10,
|
||||
"end_sequence": 10,
|
||||
"geometry_association": "unknown",
|
||||
"freshness": "current",
|
||||
"motion": "static",
|
||||
"threat": "not-threat",
|
||||
"critical_corridor_obstacle": passage,
|
||||
}],
|
||||
"notes": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"schema_version": "missioncore.m48-assisted-object-correction-session/v1",
|
||||
"pack_id": pack_id,
|
||||
"session_id": "m48-correction-session-" + "b" * 64,
|
||||
"title": "fixture",
|
||||
"revision": 7,
|
||||
"state": "saved",
|
||||
"created_at_utc": "2026-08-24T10:00:00Z",
|
||||
"updated_at_utc": "2026-08-24T11:00:00Z",
|
||||
"clips": [{
|
||||
"clip_id": "m48-clip-01",
|
||||
"start_sequence": 1,
|
||||
"end_sequence": 20,
|
||||
"review_state": "reviewed",
|
||||
"no_object": False,
|
||||
"tracklets": [
|
||||
tracklet("object-01", [0.1, 0.1, 0.2, 0.2], passage=True),
|
||||
tracklet("object-02", [0.7, 0.7, 0.8, 0.8], passage=False),
|
||||
{
|
||||
**tracklet("object-03", [0.3, 0.3, 0.4, 0.4], passage=True),
|
||||
"object_id": "proposal-10-0",
|
||||
},
|
||||
],
|
||||
"notes": None,
|
||||
}],
|
||||
"progress": {"reviewed_clip_count": 1, "clip_count": 1, "complete": True},
|
||||
"seed_summary": {
|
||||
"worker_id": "006",
|
||||
"clip_count": 1,
|
||||
"frame_count": 1,
|
||||
"object_count": 1,
|
||||
"prediction_rows_sha256": "c" * 64,
|
||||
},
|
||||
"evidence_summary": None,
|
||||
"assistance": {
|
||||
"mode": "frozen-candidate-seeded",
|
||||
"candidate_predictions_seen": True,
|
||||
"model_scores_seen": False,
|
||||
"semantic_class_task_seen": False,
|
||||
"independent_truth_eligible": False,
|
||||
},
|
||||
"authority": AUTHORITY,
|
||||
"last_save_idempotency_key": "save-7",
|
||||
"reviewer_id": None,
|
||||
"submitted_at_utc": None,
|
||||
"submission_sha256": None,
|
||||
"frozen_document_name": None,
|
||||
}
|
||||
|
||||
|
||||
def test_builds_separate_assisted_baseline_without_truth_claim(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack_id = "m48-object-quality-pack-" + "a" * 64
|
||||
pack_root = tmp_path / pack_id
|
||||
pack_root.mkdir()
|
||||
pack = SimpleNamespace(
|
||||
result_id=pack_id,
|
||||
result_root=pack_root,
|
||||
manifest={
|
||||
"identity_sha256": "a" * 64,
|
||||
"identity": {
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "source-session",
|
||||
},
|
||||
"freeze": {"prediction_rows_sha256": "c" * 64},
|
||||
},
|
||||
},
|
||||
predictions=({
|
||||
"schema_version": "missioncore.m48-frozen-prediction-row/v1",
|
||||
"clip_id": "m48-clip-01",
|
||||
"sequence": 10,
|
||||
"source_time_ns": 100,
|
||||
"terminal_outcome": "delivered",
|
||||
"terminal_reason": None,
|
||||
"free_space_claimed": False,
|
||||
"objects": [{
|
||||
"prediction_id": "proposal-10-0",
|
||||
"extent_xyxy": [0.1, 0.1, 0.2, 0.2],
|
||||
"geometry_association": "unknown",
|
||||
"freshness": "current",
|
||||
"motion": "static",
|
||||
"threat": "not-threat",
|
||||
}],
|
||||
},),
|
||||
)
|
||||
monkeypatch.setattr(regression, "read_m48_object_quality_pack", lambda _: pack)
|
||||
correction_path = tmp_path / "correction.json"
|
||||
_write_json(correction_path, _correction(pack_id))
|
||||
profile_path = REPOSITORY_ROOT / "config/perception/m48-small-static-passage-regression-v1.json"
|
||||
|
||||
result = regression.build_m48_small_static_passage_regression(
|
||||
pack_root=pack_root,
|
||||
correction_session_path=correction_path,
|
||||
profile_path=profile_path,
|
||||
output_root=tmp_path / "results",
|
||||
run_created_at_utc="2026-08-24T12:00:00Z",
|
||||
)
|
||||
|
||||
assert result.report["metrics"]["assisted_anchor_count"] == 2
|
||||
assert result.report["metrics"]["worker_recalled_anchor_count"] == 1
|
||||
assert result.report["metrics"]["worker_missed_anchor_count"] == 1
|
||||
assert result.report["metrics"]["assisted_anchor_recall"] == 0.5
|
||||
assert result.manifest["accepted"] is False
|
||||
assert result.manifest["ground_truth"] is False
|
||||
assert result.manifest["identity"]["human_lab_id"] == "M4.8"
|
||||
assert result.manifest["identity"]["experiment_id"] == (
|
||||
"m48-small-static-passage-regression/v1"
|
||||
)
|
||||
assert result.report["method"]["execution_class"] == "deterministic"
|
||||
assert all(
|
||||
row["authority"] == "operator-assisted-development-anchor-not-truth"
|
||||
for row in result.anchors
|
||||
)
|
||||
|
||||
registry = LaboratoryEvidenceRegistry.from_directory(
|
||||
REPOSITORY_ROOT / "config/laboratories"
|
||||
)
|
||||
definition = next(
|
||||
row
|
||||
for row in registry.definitions
|
||||
if row.work_id == "m48-small-static-passage-regression"
|
||||
)
|
||||
proof = verify_laboratory_evidence_result(definition, result.result_root)
|
||||
assert proof["result_id"] == result.result_id
|
||||
assert proof["artifact_count"] == 3
|
||||
|
||||
|
||||
def test_reader_rejects_changed_comparison_artifact(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pack_id = "m48-object-quality-pack-" + "a" * 64
|
||||
pack_root = tmp_path / pack_id
|
||||
pack_root.mkdir()
|
||||
pack = SimpleNamespace(
|
||||
result_id=pack_id,
|
||||
result_root=pack_root,
|
||||
manifest={
|
||||
"identity_sha256": "a" * 64,
|
||||
"identity": {
|
||||
"source": {"source_id": "RAVNOVES00", "source_session_id": "source"},
|
||||
"freeze": {"prediction_rows_sha256": "c" * 64},
|
||||
},
|
||||
},
|
||||
predictions=({
|
||||
"clip_id": "m48-clip-01",
|
||||
"sequence": 10,
|
||||
"source_time_ns": 100,
|
||||
"terminal_outcome": "delivered",
|
||||
"objects": [],
|
||||
},),
|
||||
)
|
||||
monkeypatch.setattr(regression, "read_m48_object_quality_pack", lambda _: pack)
|
||||
correction_path = tmp_path / "correction.json"
|
||||
document = _correction(pack_id)
|
||||
document["clips"][0]["tracklets"] = document["clips"][0]["tracklets"][:1]
|
||||
_write_json(correction_path, document)
|
||||
result = regression.build_m48_small_static_passage_regression(
|
||||
pack_root=pack_root,
|
||||
correction_session_path=correction_path,
|
||||
profile_path=(
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48-small-static-passage-regression-v1.json"
|
||||
),
|
||||
output_root=tmp_path / "results",
|
||||
run_created_at_utc="2026-08-24T12:00:00Z",
|
||||
)
|
||||
comparison_path = result.result_root / "comparisons.jsonl"
|
||||
comparison_path.write_bytes(comparison_path.read_bytes() + b"{}\n")
|
||||
|
||||
with pytest.raises(
|
||||
regression.M48SmallStaticRegressionError,
|
||||
match="artifact proof",
|
||||
):
|
||||
regression.read_m48_small_static_passage_regression(result.result_root)
|
||||
Reference in New Issue
Block a user