feat(lab): publish M4.8 assisted regression evidence
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v2",
|
||||
"work_id": "m48-object-centric-quality",
|
||||
"evidence_lifecycle": [
|
||||
{
|
||||
"phase": "review",
|
||||
"runtime_relative_root": "m48/object-quality-packs",
|
||||
"result_id_prefix": "m48-object-quality-pack",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.m48-object-centric-quality-pack/v1"
|
||||
},
|
||||
{
|
||||
"phase": "result",
|
||||
"runtime_relative_root": "m48/object-quality-results",
|
||||
"result_id_prefix": "m48-object-quality-result",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.m48-object-centric-quality-result/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "m48-small-static-passage-regression",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "m48/small-static-passage-regression-results",
|
||||
"result_id_prefix": "m48-small-static-passage-regression",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.m48-small-static-passage-regression-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,34 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-execution-registry/v1",
|
||||
"definitions": [
|
||||
{
|
||||
"work_id": "m48-small-static-passage-regression",
|
||||
"lifecycle": "canonical",
|
||||
"isolation": "core-adapter",
|
||||
"adapter_id": "canonical.m48-small-static-passage-regression/v1",
|
||||
"input_roles": ["pack_root", "correction_session_path", "profile_path"],
|
||||
"contracts": {
|
||||
"source": "missioncore.m48-object-centric-quality-pack/v1",
|
||||
"provider": "missioncore.m48-assisted-object-correction-session/v1",
|
||||
"graph": "missioncore.m48-assisted-anchor-comparison/v1",
|
||||
"run": "missioncore.laboratory-run/v1",
|
||||
"evidence": "missioncore.m48-small-static-passage-regression-result/v1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"work_id": "m48-object-centric-quality",
|
||||
"lifecycle": "canonical",
|
||||
"isolation": "core-adapter",
|
||||
"adapter_id": "canonical.m48-object-centric-quality/v1",
|
||||
"input_roles": ["pack_root", "truth_seal_root"],
|
||||
"contracts": {
|
||||
"source": "missioncore.m48-object-centric-quality-pack/v1",
|
||||
"provider": "missioncore.m48-object-truth-seal/v1",
|
||||
"graph": "missioncore.m48-object-centric-quality-graph/v1",
|
||||
"run": "missioncore.laboratory-run/v1",
|
||||
"evidence": "missioncore.m48-object-centric-quality-result/v1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"work_id": "m4-replay-threat",
|
||||
"lifecycle": "canonical",
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"schema_version": "missioncore.m48-object-quality-selection/v1",
|
||||
"selection_id": "m48-ravnoves00-balanced-connected-clips/v1",
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"selection_basis": "prediction-frozen-source-curation-before-independent-truth",
|
||||
"camera_frame_size": {
|
||||
"width": 800,
|
||||
"height": 600
|
||||
},
|
||||
"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
|
||||
},
|
||||
"clips": [
|
||||
{
|
||||
"clip_id": "m48-clip-01",
|
||||
"component_id": "m48-component-development-01",
|
||||
"route_block": "route-block-01",
|
||||
"time_block": "time-block-01",
|
||||
"split": "development",
|
||||
"start_sequence": 1,
|
||||
"end_sequence": 61
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-02",
|
||||
"component_id": "m48-component-development-01",
|
||||
"route_block": "route-block-01",
|
||||
"time_block": "time-block-01",
|
||||
"split": "development",
|
||||
"start_sequence": 121,
|
||||
"end_sequence": 181
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-03",
|
||||
"component_id": "m48-component-development-01",
|
||||
"route_block": "route-block-01",
|
||||
"time_block": "time-block-01",
|
||||
"split": "development",
|
||||
"start_sequence": 241,
|
||||
"end_sequence": 301
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-04",
|
||||
"component_id": "m48-component-development-02",
|
||||
"route_block": "route-block-01",
|
||||
"time_block": "time-block-02",
|
||||
"split": "development",
|
||||
"start_sequence": 421,
|
||||
"end_sequence": 481
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-05",
|
||||
"component_id": "m48-component-development-02",
|
||||
"route_block": "route-block-01",
|
||||
"time_block": "time-block-02",
|
||||
"split": "development",
|
||||
"start_sequence": 581,
|
||||
"end_sequence": 641
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-06",
|
||||
"component_id": "m48-component-development-02",
|
||||
"route_block": "route-block-02",
|
||||
"time_block": "time-block-02",
|
||||
"split": "development",
|
||||
"start_sequence": 821,
|
||||
"end_sequence": 881
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-07",
|
||||
"component_id": "m48-component-development-03",
|
||||
"route_block": "route-block-02",
|
||||
"time_block": "time-block-03",
|
||||
"split": "development",
|
||||
"start_sequence": 1041,
|
||||
"end_sequence": 1101
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-08",
|
||||
"component_id": "m48-component-development-03",
|
||||
"route_block": "route-block-02",
|
||||
"time_block": "time-block-03",
|
||||
"split": "development",
|
||||
"start_sequence": 1221,
|
||||
"end_sequence": 1281
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-09",
|
||||
"component_id": "m48-component-development-03",
|
||||
"route_block": "route-block-02",
|
||||
"time_block": "time-block-03",
|
||||
"split": "development",
|
||||
"start_sequence": 1421,
|
||||
"end_sequence": 1481
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-10",
|
||||
"component_id": "m48-component-development-04",
|
||||
"route_block": "route-block-03-development",
|
||||
"time_block": "time-block-04",
|
||||
"split": "development",
|
||||
"start_sequence": 1681,
|
||||
"end_sequence": 1741
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-11",
|
||||
"component_id": "m48-component-development-04",
|
||||
"route_block": "route-block-03-development",
|
||||
"time_block": "time-block-04",
|
||||
"split": "development",
|
||||
"start_sequence": 1830,
|
||||
"end_sequence": 1890
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-12",
|
||||
"component_id": "m48-component-development-04",
|
||||
"route_block": "route-block-03-development",
|
||||
"time_block": "time-block-04",
|
||||
"split": "development",
|
||||
"start_sequence": 2041,
|
||||
"end_sequence": 2101
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-13",
|
||||
"component_id": "m48-component-validation-01",
|
||||
"route_block": "route-block-03-validation",
|
||||
"time_block": "time-block-05",
|
||||
"split": "validation",
|
||||
"start_sequence": 2191,
|
||||
"end_sequence": 2251
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-14",
|
||||
"component_id": "m48-component-validation-01",
|
||||
"route_block": "route-block-03-validation",
|
||||
"time_block": "time-block-05",
|
||||
"split": "validation",
|
||||
"start_sequence": 2371,
|
||||
"end_sequence": 2431
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-15",
|
||||
"component_id": "m48-component-validation-01",
|
||||
"route_block": "route-block-03-validation",
|
||||
"time_block": "time-block-05",
|
||||
"split": "validation",
|
||||
"start_sequence": 2551,
|
||||
"end_sequence": 2611
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-16",
|
||||
"component_id": "m48-component-validation-02",
|
||||
"route_block": "route-block-04",
|
||||
"time_block": "time-block-06",
|
||||
"split": "validation",
|
||||
"start_sequence": 2731,
|
||||
"end_sequence": 2791
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-17",
|
||||
"component_id": "m48-component-validation-02",
|
||||
"route_block": "route-block-04",
|
||||
"time_block": "time-block-06",
|
||||
"split": "validation",
|
||||
"start_sequence": 2911,
|
||||
"end_sequence": 2971
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-18",
|
||||
"component_id": "m48-component-validation-02",
|
||||
"route_block": "route-block-04",
|
||||
"time_block": "time-block-06",
|
||||
"split": "validation",
|
||||
"start_sequence": 3111,
|
||||
"end_sequence": 3171
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-19",
|
||||
"component_id": "m48-component-validation-03",
|
||||
"route_block": "route-block-04",
|
||||
"time_block": "time-block-07",
|
||||
"split": "validation",
|
||||
"start_sequence": 3291,
|
||||
"end_sequence": 3351
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-20",
|
||||
"component_id": "m48-component-validation-03",
|
||||
"route_block": "route-block-04",
|
||||
"time_block": "time-block-07",
|
||||
"split": "validation",
|
||||
"start_sequence": 3471,
|
||||
"end_sequence": 3531
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-21",
|
||||
"component_id": "m48-component-validation-03",
|
||||
"route_block": "route-block-05",
|
||||
"time_block": "time-block-07",
|
||||
"split": "validation",
|
||||
"start_sequence": 3651,
|
||||
"end_sequence": 3711
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-22",
|
||||
"component_id": "m48-component-validation-04",
|
||||
"route_block": "route-block-05",
|
||||
"time_block": "time-block-08",
|
||||
"split": "validation",
|
||||
"start_sequence": 3831,
|
||||
"end_sequence": 3891
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-23",
|
||||
"component_id": "m48-component-validation-04",
|
||||
"route_block": "route-block-05",
|
||||
"time_block": "time-block-08",
|
||||
"split": "validation",
|
||||
"start_sequence": 4051,
|
||||
"end_sequence": 4111
|
||||
},
|
||||
{
|
||||
"clip_id": "m48-clip-24",
|
||||
"component_id": "m48-component-validation-04",
|
||||
"route_block": "route-block-05",
|
||||
"time_block": "time-block-08",
|
||||
"split": "validation",
|
||||
"start_sequence": 4429,
|
||||
"end_sequence": 4489
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"schema_version": "missioncore.m48-object-quality-profile/v1",
|
||||
"profile_id": "m48-ravnoves00-object-quality/v1",
|
||||
"source_graph_id": "reference-perception-graph/v2",
|
||||
"source_profile_id": "m4-ravnoves00-recorded-realtime/v1",
|
||||
"clip_contract": {
|
||||
"minimum_clip_count": 20,
|
||||
"maximum_clip_count": 30,
|
||||
"minimum_duration_seconds": 5.0,
|
||||
"maximum_duration_seconds": 10.0,
|
||||
"required_validation_hypotheses": [
|
||||
"prediction-associated",
|
||||
"prediction-fisheye-edge",
|
||||
"prediction-moving",
|
||||
"prediction-small-obstacle",
|
||||
"prediction-sparse-scene",
|
||||
"prediction-static",
|
||||
"prediction-threat",
|
||||
"prediction-unassociated"
|
||||
],
|
||||
"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
|
||||
},
|
||||
"splits": [
|
||||
"development",
|
||||
"validation"
|
||||
],
|
||||
"connected_component_split_overlap_allowed": false,
|
||||
"route_or_time_block_split_overlap_allowed": false,
|
||||
"release_gate_split": "validation"
|
||||
},
|
||||
"review_contract": {
|
||||
"review_unit": "clip-local-object-tracklet",
|
||||
"extent_labels": "sparse-normalized-xyxy-keyframes",
|
||||
"state_labels": "contiguous-tracklet-state-segments",
|
||||
"per_frame_expansion": {
|
||||
"extent": "linear-between-bounding-keyframes",
|
||||
"visibility": "left-keyframe-hold",
|
||||
"state": "contiguous-state-segment"
|
||||
},
|
||||
"semantic_class_labels_allowed": false,
|
||||
"independent_reviewers_required": 2,
|
||||
"adjudication_required": true,
|
||||
"predictions_frozen_before_label_reveal": true,
|
||||
"prediction_content_visible_to_reviewers": false,
|
||||
"selection_hypotheses_visible_to_reviewers": false
|
||||
},
|
||||
"matching": {
|
||||
"extent_iou_threshold": 0.5
|
||||
},
|
||||
"release_thresholds": {
|
||||
"terminal_outcome_accounting": 1.0,
|
||||
"false_free_space_claims": 0,
|
||||
"obstacle_presence_precision": 0.9,
|
||||
"obstacle_presence_recall": 0.9,
|
||||
"critical_corridor_obstacle_recall": 0.95,
|
||||
"geometry_association_correctness": 0.9,
|
||||
"freshness_correctness": 0.9,
|
||||
"motion_decision_correctness": 0.9,
|
||||
"critical_threat_not_threat": 0
|
||||
},
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": false,
|
||||
"commands_enabled": false,
|
||||
"actuation_allowed": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"schema_version": "missioncore.m48-small-static-passage-regression-profile/v1",
|
||||
"profile_id": "m48-small-static-passage-regression/v1",
|
||||
"pipeline_id": "m48-class-free-object-quality/v1",
|
||||
"experiment_id": "m48-small-static-passage-regression/v1",
|
||||
"human_lab_id": "M4.8",
|
||||
"run_label": "M4.8R1",
|
||||
"anchor_selection": "operator-added-tracklets-in-reviewed-clips/v1",
|
||||
"extent_iou_threshold": 0.5,
|
||||
"minimum_assisted_anchor_recall": 0.9,
|
||||
"minimum_anchor_count": 1,
|
||||
"independent_truth": false
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze the source-scoped RAVNOVES00 M4.8 independent-review pack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.laboratory.m48_ravnoves00_pack import prepare_m48_ravnoves00_pack
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--m47-lab-root", type=Path, required=True)
|
||||
parser.add_argument("--graph-result-root", type=Path, required=True)
|
||||
parser.add_argument("--threat-result-root", type=Path, required=True)
|
||||
parser.add_argument("--geometry-result-root", type=Path, required=True)
|
||||
parser.add_argument("--camera-index", type=Path, required=True)
|
||||
parser.add_argument("--selection", type=Path, required=True)
|
||||
parser.add_argument("--frozen-at-utc", required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = prepare_m48_ravnoves00_pack(
|
||||
m47_lab_root=args.m47_lab_root,
|
||||
graph_result_root=args.graph_result_root,
|
||||
threat_result_root=args.threat_result_root,
|
||||
geometry_result_root=args.geometry_result_root,
|
||||
camera_index_path=args.camera_index,
|
||||
selection_path=args.selection,
|
||||
frozen_at_utc=args.frozen_at_utc,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"status": result.report["status"],
|
||||
"clip_count": result.report["metrics"]["clip_count"],
|
||||
"frame_count": result.report["metrics"]["frame_count"],
|
||||
"truth_labels_available": False,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish one canonical append-only M4.8 small-static regression run."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.pipeline_telemetry import JsonlPipelineTelemetrySink
|
||||
from k1link.laboratory import (
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryExecutionRegistry,
|
||||
LaboratoryRunner,
|
||||
LaboratoryRunRequest,
|
||||
)
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--pack-root", type=Path, required=True)
|
||||
parser.add_argument("--correction-session", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
parser.add_argument("--receipt-root", type=Path, required=True)
|
||||
parser.add_argument("--telemetry-path", type=Path, required=True)
|
||||
parser.add_argument("--run-id", required=True)
|
||||
parser.add_argument("--request-id", required=True)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parser().parse_args()
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
evidence = LaboratoryEvidenceRegistry.from_directory(
|
||||
repository_root / "config" / "laboratories"
|
||||
)
|
||||
execution = LaboratoryExecutionRegistry.from_file(
|
||||
repository_root / "config" / "laboratory-execution.json",
|
||||
evidence,
|
||||
)
|
||||
runner = LaboratoryRunner(
|
||||
registry=execution,
|
||||
evidence_registry=evidence,
|
||||
sink=JsonlPipelineTelemetrySink(args.telemetry_path),
|
||||
)
|
||||
pack_id = args.pack_root.name
|
||||
result = runner.run(
|
||||
LaboratoryRunRequest(
|
||||
work_id="m48-small-static-passage-regression",
|
||||
run_id=args.run_id,
|
||||
request_id=args.request_id,
|
||||
contour_id="mission-core-laboratory",
|
||||
agent_id="local-control-plane",
|
||||
node_id=socket.gethostname(),
|
||||
source_id="RAVNOVES00",
|
||||
source_package_id=pack_id,
|
||||
method_id="m48-small-static-passage-regression/v1",
|
||||
inputs={
|
||||
"pack_root": args.pack_root,
|
||||
"correction_session_path": args.correction_session,
|
||||
"profile_path": args.profile,
|
||||
},
|
||||
output_root=args.output_root,
|
||||
receipt_root=args.receipt_root,
|
||||
)
|
||||
)
|
||||
print(json.dumps({
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"receipt_id": result.receipt_id,
|
||||
"receipt_root": str(result.receipt_root),
|
||||
}, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
from k1link.laboratory.evidence_registry import (
|
||||
LABORATORY_EVIDENCE_DEFINITION_SCHEMA,
|
||||
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA,
|
||||
LaboratoryEvidenceDefinition,
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryEvidenceVariant,
|
||||
LaboratoryRegistryError,
|
||||
)
|
||||
from k1link.laboratory.evidence_report import (
|
||||
@@ -34,9 +36,11 @@ from k1link.laboratory.value_review_registry import (
|
||||
|
||||
__all__ = [
|
||||
"LABORATORY_EVIDENCE_DEFINITION_SCHEMA",
|
||||
"LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA",
|
||||
"LABORATORY_EVIDENCE_REPORT_SCHEMA",
|
||||
"LaboratoryEvidenceDefinition",
|
||||
"LaboratoryEvidenceRegistry",
|
||||
"LaboratoryEvidenceVariant",
|
||||
"LaboratoryEvidenceReportError",
|
||||
"LaboratoryEvidenceReportNotFound",
|
||||
"LaboratoryEvidenceReportService",
|
||||
|
||||
@@ -7,13 +7,22 @@ from pathlib import Path, PurePosixPath
|
||||
from typing import Final
|
||||
|
||||
LABORATORY_EVIDENCE_DEFINITION_SCHEMA: Final = "missioncore.laboratory-evidence-definition/v1"
|
||||
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA: Final = (
|
||||
"missioncore.laboratory-evidence-definition/v2"
|
||||
)
|
||||
_DEFINITION_MAX_BYTES: Final = 16 * 1024
|
||||
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
|
||||
_SCHEMA_VERSION = re.compile(r"^missioncore\.[a-z0-9.-]+/v[1-9][0-9]*$")
|
||||
_TOP_LEVEL_KEYS: Final = frozenset({"schema_version", "work_id", "evidence"})
|
||||
_LIFECYCLE_TOP_LEVEL_KEYS: Final = frozenset(
|
||||
{"schema_version", "work_id", "evidence_lifecycle"}
|
||||
)
|
||||
_EVIDENCE_KEYS: Final = frozenset(
|
||||
{"runtime_relative_root", "result_id_prefix", "document_name", "schema_version"}
|
||||
)
|
||||
_LIFECYCLE_EVIDENCE_KEYS: Final = frozenset(
|
||||
{"phase", "runtime_relative_root", "result_id_prefix", "document_name", "schema_version"}
|
||||
)
|
||||
|
||||
|
||||
class LaboratoryRegistryError(ValueError):
|
||||
@@ -21,15 +30,15 @@ class LaboratoryRegistryError(ValueError):
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryEvidenceDefinition:
|
||||
work_id: str
|
||||
class LaboratoryEvidenceVariant:
|
||||
phase: str
|
||||
runtime_relative_root: PurePosixPath
|
||||
result_id_prefix: str
|
||||
document_name: str
|
||||
result_schema_version: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.work_id, "work_id")
|
||||
_identifier(self.phase, "evidence phase")
|
||||
_identifier(self.result_id_prefix, "result_id_prefix")
|
||||
_document_name(self.document_name)
|
||||
_schema_version(self.result_schema_version)
|
||||
@@ -45,6 +54,69 @@ class LaboratoryEvidenceDefinition:
|
||||
return runtime_root.joinpath(*self.runtime_relative_root.parts)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryEvidenceDefinition:
|
||||
work_id: str
|
||||
runtime_relative_root: PurePosixPath
|
||||
result_id_prefix: str
|
||||
document_name: str
|
||||
result_schema_version: str
|
||||
lifecycle_variants: tuple[LaboratoryEvidenceVariant, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.work_id, "work_id")
|
||||
primary = LaboratoryEvidenceVariant(
|
||||
phase="result",
|
||||
runtime_relative_root=self.runtime_relative_root,
|
||||
result_id_prefix=self.result_id_prefix,
|
||||
document_name=self.document_name,
|
||||
result_schema_version=self.result_schema_version,
|
||||
)
|
||||
if not self.lifecycle_variants:
|
||||
return
|
||||
if not all(
|
||||
isinstance(variant, LaboratoryEvidenceVariant)
|
||||
for variant in self.lifecycle_variants
|
||||
):
|
||||
raise LaboratoryRegistryError("LAB lifecycle variants must be immutable evidence")
|
||||
if self.lifecycle_variants[-1] != primary:
|
||||
raise LaboratoryRegistryError("LAB lifecycle terminal evidence must be primary")
|
||||
phases = [variant.phase for variant in self.lifecycle_variants]
|
||||
if len(phases) != len(set(phases)):
|
||||
raise LaboratoryRegistryError("duplicate LAB evidence phase")
|
||||
|
||||
@property
|
||||
def evidence_variants(self) -> tuple[LaboratoryEvidenceVariant, ...]:
|
||||
if self.lifecycle_variants:
|
||||
return self.lifecycle_variants
|
||||
return (
|
||||
LaboratoryEvidenceVariant(
|
||||
phase="result",
|
||||
runtime_relative_root=self.runtime_relative_root,
|
||||
result_id_prefix=self.result_id_prefix,
|
||||
document_name=self.document_name,
|
||||
result_schema_version=self.result_schema_version,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def result_id_pattern(self) -> re.Pattern[str]:
|
||||
return re.compile(rf"^{re.escape(self.result_id_prefix)}-[a-f0-9]{{64}}$")
|
||||
|
||||
def result_root(self, runtime_root: Path) -> Path:
|
||||
return runtime_root.joinpath(*self.runtime_relative_root.parts)
|
||||
|
||||
def variant_for_result_id(self, result_id: str) -> LaboratoryEvidenceVariant | None:
|
||||
return next(
|
||||
(
|
||||
variant
|
||||
for variant in self.evidence_variants
|
||||
if variant.result_id_pattern.fullmatch(result_id) is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryEvidenceRegistry:
|
||||
definitions: tuple[LaboratoryEvidenceDefinition, ...]
|
||||
@@ -89,14 +161,42 @@ def _read_definition(path: Path) -> LaboratoryEvidenceDefinition:
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
raise LaboratoryRegistryError(f"LAB definition is unreadable: {path.name}") from exc
|
||||
document = _object(payload, f"LAB definition {path.name}")
|
||||
_exact_keys(document, _TOP_LEVEL_KEYS, f"LAB definition {path.name}")
|
||||
if document["schema_version"] != LABORATORY_EVIDENCE_DEFINITION_SCHEMA:
|
||||
schema_version = document.get("schema_version")
|
||||
if schema_version not in {
|
||||
LABORATORY_EVIDENCE_DEFINITION_SCHEMA,
|
||||
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA,
|
||||
}:
|
||||
raise LaboratoryRegistryError(f"LAB definition schema is invalid: {path.name}")
|
||||
expected_keys = (
|
||||
_TOP_LEVEL_KEYS
|
||||
if schema_version == LABORATORY_EVIDENCE_DEFINITION_SCHEMA
|
||||
else _LIFECYCLE_TOP_LEVEL_KEYS
|
||||
)
|
||||
_exact_keys(document, expected_keys, f"LAB definition {path.name}")
|
||||
work_id = _identifier(document["work_id"], "work_id")
|
||||
if path.name != f"{work_id}.json":
|
||||
raise LaboratoryRegistryError(f"LAB definition filename must match work_id: {path.name}")
|
||||
evidence = _object(document["evidence"], f"LAB evidence {work_id}")
|
||||
_exact_keys(evidence, _EVIDENCE_KEYS, f"LAB evidence {work_id}")
|
||||
if schema_version == LABORATORY_EVIDENCE_DEFINITION_SCHEMA:
|
||||
lifecycle_variants: tuple[LaboratoryEvidenceVariant, ...] = ()
|
||||
evidence = _object(document["evidence"], f"LAB evidence {work_id}")
|
||||
_exact_keys(evidence, _EVIDENCE_KEYS, f"LAB evidence {work_id}")
|
||||
else:
|
||||
lifecycle = document["evidence_lifecycle"]
|
||||
if not isinstance(lifecycle, list) or len(lifecycle) < 2:
|
||||
raise LaboratoryRegistryError(
|
||||
f"LAB evidence lifecycle must contain at least two phases: {work_id}"
|
||||
)
|
||||
lifecycle_variants = tuple(
|
||||
_read_variant(row, f"LAB evidence {work_id}[{index}]")
|
||||
for index, row in enumerate(lifecycle)
|
||||
)
|
||||
terminal = lifecycle_variants[-1]
|
||||
evidence = {
|
||||
"runtime_relative_root": str(terminal.runtime_relative_root),
|
||||
"result_id_prefix": terminal.result_id_prefix,
|
||||
"document_name": terminal.document_name,
|
||||
"schema_version": terminal.result_schema_version,
|
||||
}
|
||||
result_id_prefix = _identifier(evidence["result_id_prefix"], "result_id_prefix")
|
||||
document_name = _document_name(evidence["document_name"])
|
||||
result_schema_version = _schema_version(evidence["schema_version"])
|
||||
@@ -106,6 +206,19 @@ def _read_definition(path: Path) -> LaboratoryEvidenceDefinition:
|
||||
result_id_prefix=result_id_prefix,
|
||||
document_name=document_name,
|
||||
result_schema_version=result_schema_version,
|
||||
lifecycle_variants=lifecycle_variants,
|
||||
)
|
||||
|
||||
|
||||
def _read_variant(value: object, label: str) -> LaboratoryEvidenceVariant:
|
||||
evidence = _object(value, label)
|
||||
_exact_keys(evidence, _LIFECYCLE_EVIDENCE_KEYS, label)
|
||||
return LaboratoryEvidenceVariant(
|
||||
phase=_identifier(evidence["phase"], f"{label}.phase"),
|
||||
runtime_relative_root=_relative_root(evidence["runtime_relative_root"]),
|
||||
result_id_prefix=_identifier(evidence["result_id_prefix"], "result_id_prefix"),
|
||||
document_name=_document_name(evidence["document_name"]),
|
||||
result_schema_version=_schema_version(evidence["schema_version"]),
|
||||
)
|
||||
|
||||
|
||||
@@ -177,9 +290,15 @@ def _relative_root(value: object) -> PurePosixPath:
|
||||
def _reject_duplicates(definitions: tuple[LaboratoryEvidenceDefinition, ...]) -> None:
|
||||
dimensions = {
|
||||
"work_id": [definition.work_id for definition in definitions],
|
||||
"result_id_prefix": [definition.result_id_prefix for definition in definitions],
|
||||
"result_id_prefix": [
|
||||
variant.result_id_prefix
|
||||
for definition in definitions
|
||||
for variant in definition.evidence_variants
|
||||
],
|
||||
"runtime_relative_root": [
|
||||
str(definition.runtime_relative_root) for definition in definitions
|
||||
str(variant.runtime_relative_root)
|
||||
for definition in definitions
|
||||
for variant in definition.evidence_variants
|
||||
],
|
||||
}
|
||||
for label, values in dimensions.items():
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, Final
|
||||
from k1link.laboratory.evidence_registry import (
|
||||
LaboratoryEvidenceDefinition,
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryEvidenceVariant,
|
||||
)
|
||||
|
||||
LABORATORY_EVIDENCE_REPORT_SCHEMA: Final = "missioncore.laboratory-evidence-report/v1"
|
||||
@@ -40,12 +41,13 @@ def verify_laboratory_evidence_result(
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise LaboratoryEvidenceReportError("LAB evidence result is unavailable") from exc
|
||||
if not resolved.is_dir() or definition.result_id_pattern.fullmatch(resolved.name) is None:
|
||||
variant = definition.variant_for_result_id(resolved.name)
|
||||
if not resolved.is_dir() or variant is None:
|
||||
raise LaboratoryEvidenceReportError("LAB evidence result path is invalid")
|
||||
document_path = _safe_file(resolved, definition.document_name)
|
||||
document_path = _safe_file(resolved, variant.document_name)
|
||||
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
|
||||
document = _json_object(document_bytes, "LAB document")
|
||||
_validate_document(document, definition, resolved.name)
|
||||
_validate_document(document, variant, resolved.name)
|
||||
identity = _object_or_none(document.get("identity"))
|
||||
identity_sha256 = document.get("identity_sha256")
|
||||
if identity is None or not isinstance(identity_sha256, str):
|
||||
@@ -77,13 +79,14 @@ class LaboratoryEvidenceReportService:
|
||||
|
||||
def read(self, work_id: str, result_id: str) -> dict[str, object]:
|
||||
definition = self._definitions.get(work_id)
|
||||
if definition is None or definition.result_id_pattern.fullmatch(result_id) is None:
|
||||
variant = definition.variant_for_result_id(result_id) if definition is not None else None
|
||||
if definition is None or variant is None:
|
||||
raise LaboratoryEvidenceReportNotFound("LAB evidence identity is unknown")
|
||||
result_root = self._result_root(definition, result_id)
|
||||
document_path = _safe_file(result_root, definition.document_name)
|
||||
result_root = self._result_root(variant, result_id)
|
||||
document_path = _safe_file(result_root, variant.document_name)
|
||||
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
|
||||
document = _json_object(document_bytes, "LAB document")
|
||||
_validate_document(document, definition, result_id)
|
||||
_validate_document(document, variant, result_id)
|
||||
|
||||
identity = _object_or_none(document.get("identity"))
|
||||
identity_sha256 = document.get("identity_sha256")
|
||||
@@ -210,7 +213,7 @@ class LaboratoryEvidenceReportService:
|
||||
|
||||
def _result_root(
|
||||
self,
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
variant: LaboratoryEvidenceVariant,
|
||||
result_id: str,
|
||||
) -> Path:
|
||||
configured = self._runtime_root_provider()
|
||||
@@ -223,7 +226,7 @@ class LaboratoryEvidenceReportService:
|
||||
runtime_root = runtime_root.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable") from exc
|
||||
candidate = definition.result_root(runtime_root) / result_id
|
||||
candidate = variant.result_root(runtime_root) / result_id
|
||||
if candidate.is_symlink():
|
||||
raise LaboratoryEvidenceReportError("LAB result must not be a symlink")
|
||||
try:
|
||||
@@ -237,7 +240,7 @@ class LaboratoryEvidenceReportService:
|
||||
|
||||
def _validate_document(
|
||||
document: dict[str, Any],
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
definition: LaboratoryEvidenceVariant,
|
||||
result_id: str,
|
||||
) -> None:
|
||||
if document.get("schema_version") != definition.result_schema_version:
|
||||
|
||||
@@ -169,10 +169,11 @@ class LaboratoryExecutionRegistry:
|
||||
f"laboratory classification is incomplete; missing={missing}, unknown={unknown}"
|
||||
)
|
||||
for definition in self.definitions:
|
||||
if (
|
||||
evidence_by_work_id[definition.work_id].result_schema_version
|
||||
!= definition.evidence_contract
|
||||
):
|
||||
evidence_contracts = {
|
||||
variant.result_schema_version
|
||||
for variant in evidence_by_work_id[definition.work_id].evidence_variants
|
||||
}
|
||||
if definition.evidence_contract not in evidence_contracts:
|
||||
raise LaboratoryExecutionError(
|
||||
f"laboratory evidence contract mismatch: {definition.work_id}"
|
||||
)
|
||||
@@ -311,6 +312,10 @@ class LaboratoryRunner:
|
||||
|
||||
def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
||||
return {
|
||||
"canonical.m48-small-static-passage-regression/v1": (
|
||||
_run_m48_small_static_passage_regression
|
||||
),
|
||||
"canonical.m48-object-centric-quality/v1": _run_m48_object_centric_quality,
|
||||
"canonical.m4-replay-threat/v1": _run_m4_replay_threat,
|
||||
"canonical.e33-worker-shadow/v1": _run_e33,
|
||||
"canonical.e35-degradation-recovery/v1": _run_e35,
|
||||
@@ -319,6 +324,41 @@ def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
||||
}
|
||||
|
||||
|
||||
def _run_m48_small_static_passage_regression(
|
||||
request: LaboratoryRunRequest,
|
||||
) -> LaboratoryAdapterResult:
|
||||
from k1link.laboratory.m48_small_static_regression import (
|
||||
build_m48_small_static_passage_regression,
|
||||
)
|
||||
|
||||
result = build_m48_small_static_passage_regression(
|
||||
pack_root=request.inputs["pack_root"],
|
||||
correction_session_path=request.inputs["correction_session_path"],
|
||||
profile_path=request.inputs["profile_path"],
|
||||
output_root=request.output_root,
|
||||
)
|
||||
return LaboratoryAdapterResult(
|
||||
result_root=result.result_root,
|
||||
result_id=result.result_id,
|
||||
)
|
||||
|
||||
|
||||
def _run_m48_object_centric_quality(
|
||||
request: LaboratoryRunRequest,
|
||||
) -> LaboratoryAdapterResult:
|
||||
from k1link.laboratory.m48_object_quality import score_m48_object_quality
|
||||
|
||||
result = score_m48_object_quality(
|
||||
pack_root=request.inputs["pack_root"],
|
||||
truth_seal_root=request.inputs["truth_seal_root"],
|
||||
output_root=request.output_root,
|
||||
)
|
||||
return LaboratoryAdapterResult(
|
||||
result_root=result.result_root,
|
||||
result_id=result.result_id,
|
||||
)
|
||||
|
||||
|
||||
def _run_m4_replay_threat(request: LaboratoryRunRequest) -> LaboratoryAdapterResult:
|
||||
from k1link.perception.threat_replay import build_threat_replay
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,567 @@
|
||||
"""Deterministic RAVNOVES00 adapter for the M4.8 object-quality pack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Iterator, Mapping
|
||||
from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.laboratory.m47_reference_graph import read_m47_reference_graph_lab
|
||||
from k1link.laboratory.m48_object_quality import (
|
||||
M48_PREPARATION_PROVENANCE_SCHEMA,
|
||||
M48_SELECTION_HYPOTHESIS_PROFILE,
|
||||
M48ObjectQualityPack,
|
||||
build_m48_object_quality_pack,
|
||||
)
|
||||
|
||||
M48_SELECTION_SCHEMA: Final = "missioncore.m48-object-quality-selection/v1"
|
||||
M48_SELECTION_ID: Final = "m48-ravnoves00-balanced-connected-clips/v1"
|
||||
M48_SOURCE_ID: Final = "RAVNOVES00"
|
||||
M48_SOURCE_SESSION_ID: Final = "20260720T065719Z_viewer_live"
|
||||
M48_FRAME_COUNT: Final = 4_489
|
||||
M48_IMAGE_WIDTH: Final = 800
|
||||
M48_IMAGE_HEIGHT: Final = 600
|
||||
_CAMERA_INDEX_SCHEMA: Final = "missioncore.camera-recording-index/v1"
|
||||
_GRAPH_FRAME_SCHEMA: Final = "missioncore.local-obstacle-map/v1"
|
||||
_THREAT_FRAME_SCHEMAS: Final = frozenset(
|
||||
{
|
||||
"missioncore.perception-threat-replay-frame/v1",
|
||||
"missioncore.perception-threat-replay-frame/v2",
|
||||
}
|
||||
)
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
class M48Ravnoves00PackError(RuntimeError):
|
||||
"""The source adapter escaped the accepted immutable RAVNOVES00 evidence."""
|
||||
|
||||
|
||||
def prepare_m48_ravnoves00_pack(
|
||||
*,
|
||||
m47_lab_root: Path,
|
||||
graph_result_root: Path,
|
||||
threat_result_root: Path,
|
||||
geometry_result_root: Path,
|
||||
camera_index_path: Path,
|
||||
selection_path: Path,
|
||||
frozen_at_utc: str,
|
||||
output_root: Path,
|
||||
) -> M48ObjectQualityPack:
|
||||
"""Freeze the selected M4.8 clips from the exact accepted M4.7 source."""
|
||||
|
||||
lab = read_m47_reference_graph_lab(m47_lab_root)
|
||||
source = _mapping(lab.report.get("source"), "M4.7 source")
|
||||
graph_root = _directory(graph_result_root, "M4.7 graph result")
|
||||
threat_root = _directory(threat_result_root, "M4.6 visual result")
|
||||
geometry_root = _directory(geometry_result_root, "M4.4 geometry result")
|
||||
camera_index = _file(camera_index_path, "recorded camera index")
|
||||
selection = _read_json(_file(selection_path, "M4.8 selection"), "M4.8 selection")
|
||||
clips = _selection_clips(selection)
|
||||
|
||||
if (
|
||||
graph_root.name != source.get("graph_result_id")
|
||||
or threat_root.name != source.get("visual_result_id")
|
||||
or source.get("source_id") != M48_SOURCE_ID
|
||||
or source.get("source_session_id") != M48_SOURCE_SESSION_ID
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.8 source roots do not match the accepted M4.7 LAB")
|
||||
|
||||
graph_frames_path = _validate_graph_result(graph_root)
|
||||
threat_frames_path, threat_identity = _validate_threat_result(
|
||||
threat_root,
|
||||
expected_frames_sha256=source.get("threat_frames_sha256"),
|
||||
)
|
||||
geometry_frames_path = _validate_geometry_result(
|
||||
geometry_root,
|
||||
expected_result_id=threat_identity.get("geometry_result_id"),
|
||||
expected_frames_sha256=threat_identity.get("geometry_frames_sha256"),
|
||||
)
|
||||
camera_rows = tuple(_iter_jsonl(camera_index, "recorded camera index"))
|
||||
_validate_camera_rows(camera_rows)
|
||||
|
||||
selected_sequences = {
|
||||
sequence
|
||||
for clip in clips
|
||||
for sequence in range(
|
||||
_integer(clip.get("start_sequence"), "clip start_sequence"),
|
||||
_integer(clip.get("end_sequence"), "clip end_sequence") + 1,
|
||||
)
|
||||
}
|
||||
frame_catalog: list[dict[str, object]] = []
|
||||
predictions: list[dict[str, object]] = []
|
||||
previous_source_time_ns = -1
|
||||
graph_rows = _iter_jsonl(graph_frames_path, "M4.7 graph frames")
|
||||
threat_rows = _iter_jsonl(threat_frames_path, "M4.6 threat frames")
|
||||
geometry_rows = _iter_jsonl(geometry_frames_path, "M4.4 geometry frames")
|
||||
for frame_index, values in enumerate(
|
||||
zip_longest(graph_rows, threat_rows, geometry_rows, camera_rows),
|
||||
):
|
||||
graph_row, threat_row, geometry_row, camera_row = values
|
||||
if graph_row is None or threat_row is None or geometry_row is None or camera_row is None:
|
||||
raise M48Ravnoves00PackError("M4.8 source ledgers have different lengths")
|
||||
sequence = frame_index + 1
|
||||
source_time_ns = _validate_bound_frame(
|
||||
graph_row=graph_row,
|
||||
threat_row=threat_row,
|
||||
geometry_row=geometry_row,
|
||||
camera_row=camera_row,
|
||||
frame_index=frame_index,
|
||||
previous_source_time_ns=previous_source_time_ns,
|
||||
)
|
||||
previous_source_time_ns = source_time_ns
|
||||
frame_catalog.append(
|
||||
{
|
||||
"sequence": sequence,
|
||||
"source_time_ns": source_time_ns,
|
||||
"camera_fragment_sha256": camera_row["sha256"],
|
||||
}
|
||||
)
|
||||
if sequence in selected_sequences:
|
||||
obstacle_map = _mapping(graph_row.get("obstacle_map"), "M4.7 obstacle map")
|
||||
predictions.append(
|
||||
{
|
||||
"sequence": sequence,
|
||||
"source_time_ns": source_time_ns,
|
||||
"terminal_outcome": "delivered",
|
||||
"terminal_reason": None,
|
||||
"free_space_claimed": obstacle_map["free_space_claimed"],
|
||||
"objects": _prediction_objects(
|
||||
threat_row.get("camera_proposals"),
|
||||
geometry_observations=geometry_row.get("observations"),
|
||||
metric_obstacles=threat_row.get("metric_obstacles"),
|
||||
),
|
||||
}
|
||||
)
|
||||
if len(frame_catalog) != M48_FRAME_COUNT:
|
||||
raise M48Ravnoves00PackError("M4.8 source frame count changed")
|
||||
|
||||
preparation_provenance = {
|
||||
"schema_version": M48_PREPARATION_PROVENANCE_SCHEMA,
|
||||
"adapter": {
|
||||
"module": "k1link.laboratory.m48_ravnoves00_pack",
|
||||
"sha256": _file_sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
"selection": {
|
||||
"selection_id": M48_SELECTION_ID,
|
||||
"sha256": _file_sha256(selection_path),
|
||||
},
|
||||
"camera_index": {
|
||||
"source_session_id": M48_SOURCE_SESSION_ID,
|
||||
"sha256": _file_sha256(camera_index),
|
||||
"byte_length": camera_index.stat().st_size,
|
||||
"frame_count": len(camera_rows),
|
||||
},
|
||||
"graph": _source_provenance(graph_root, graph_frames_path),
|
||||
"threat": _source_provenance(threat_root, threat_frames_path),
|
||||
"geometry": _source_provenance(geometry_root, geometry_frames_path),
|
||||
}
|
||||
|
||||
return build_m48_object_quality_pack(
|
||||
m47_lab_root=lab.result_root,
|
||||
frame_catalog=frame_catalog,
|
||||
clips=clips,
|
||||
predictions=predictions,
|
||||
preparation_provenance=preparation_provenance,
|
||||
frozen_at_utc=frozen_at_utc,
|
||||
output_root=output_root,
|
||||
)
|
||||
|
||||
|
||||
def _validate_graph_result(root: Path) -> Path:
|
||||
manifest = _read_json(_file(root / "manifest.json", "M4.7 graph manifest"), "graph manifest")
|
||||
files = _mapping(manifest.get("files"), "M4.7 graph files")
|
||||
descriptor = _mapping(files.get("frames.jsonl"), "M4.7 graph frame descriptor")
|
||||
frames = _file(root / "frames.jsonl", "M4.7 graph frames")
|
||||
expected_bytes = descriptor.get("bytes")
|
||||
expected_sha256 = descriptor.get("sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != "missioncore.reference-perception-graph-manifest/v1"
|
||||
or manifest.get("result_id") != root.name
|
||||
or manifest.get("accepted") is not True
|
||||
or manifest.get("graph_id") != "reference-perception-graph/v2"
|
||||
or manifest.get("run_mode") != "lossless-replay"
|
||||
or not isinstance(expected_bytes, int)
|
||||
or expected_bytes != frames.stat().st_size
|
||||
or not _is_sha256(expected_sha256)
|
||||
or _file_sha256(frames) != expected_sha256
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.7 graph result changed")
|
||||
return frames
|
||||
|
||||
|
||||
def _validate_threat_result(
|
||||
root: Path,
|
||||
*,
|
||||
expected_frames_sha256: object,
|
||||
) -> tuple[Path, dict[str, Any]]:
|
||||
manifest = _read_json(
|
||||
_file(root / "manifest.json", "M4.6 threat manifest"),
|
||||
"threat manifest",
|
||||
)
|
||||
identity = _mapping(manifest.get("identity"), "M4.6 threat identity")
|
||||
frames = _file(root / "frames.jsonl", "M4.6 threat frames")
|
||||
if (
|
||||
manifest.get("schema_version") != "missioncore.perception-threat-replay-result/v2"
|
||||
or manifest.get("result_id") != root.name
|
||||
or manifest.get("accepted") is not True
|
||||
or identity.get("source_session_id") != M48_SOURCE_SESSION_ID
|
||||
or not _is_sha256(expected_frames_sha256)
|
||||
or identity.get("frames_sha256") != expected_frames_sha256
|
||||
or _file_sha256(frames) != expected_frames_sha256
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.6 threat result changed")
|
||||
return frames, identity
|
||||
|
||||
|
||||
def _validate_geometry_result(
|
||||
root: Path,
|
||||
*,
|
||||
expected_result_id: object,
|
||||
expected_frames_sha256: object,
|
||||
) -> Path:
|
||||
manifest = _read_json(
|
||||
_file(root / "manifest.json", "M4.4 geometry manifest"),
|
||||
"geometry manifest",
|
||||
)
|
||||
identity = _mapping(manifest.get("identity"), "M4.4 geometry identity")
|
||||
frames = _file(root / "frames.jsonl", "M4.4 geometry frames")
|
||||
if (
|
||||
manifest.get("schema_version") != "missioncore.perception-geometry-replay-result/v1"
|
||||
or root.name != expected_result_id
|
||||
or identity.get("accepted") is not True
|
||||
or identity.get("source_pack_id")
|
||||
!= "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
||||
or not _is_sha256(expected_frames_sha256)
|
||||
or identity.get("frames_sha256") != expected_frames_sha256
|
||||
or _file_sha256(frames) != expected_frames_sha256
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.4 geometry result changed")
|
||||
return frames
|
||||
|
||||
|
||||
def _selection_clips(document: Mapping[str, object]) -> tuple[dict[str, object], ...]:
|
||||
expected_keys = {
|
||||
"schema_version",
|
||||
"selection_id",
|
||||
"source_id",
|
||||
"source_session_id",
|
||||
"selection_basis",
|
||||
"camera_frame_size",
|
||||
"selection_hypothesis_profile",
|
||||
"clips",
|
||||
}
|
||||
frame_size = _mapping(document.get("camera_frame_size"), "selection frame size")
|
||||
raw_clips = document.get("clips")
|
||||
if (
|
||||
set(document) != expected_keys
|
||||
or document.get("schema_version") != M48_SELECTION_SCHEMA
|
||||
or document.get("selection_id") != M48_SELECTION_ID
|
||||
or document.get("source_id") != M48_SOURCE_ID
|
||||
or document.get("source_session_id") != M48_SOURCE_SESSION_ID
|
||||
or document.get("selection_basis")
|
||||
!= "prediction-frozen-source-curation-before-independent-truth"
|
||||
or frame_size != {"width": M48_IMAGE_WIDTH, "height": M48_IMAGE_HEIGHT}
|
||||
or document.get("selection_hypothesis_profile") != M48_SELECTION_HYPOTHESIS_PROFILE
|
||||
or not isinstance(raw_clips, list)
|
||||
or any(not isinstance(item, dict) for item in raw_clips)
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.8 selection contract changed")
|
||||
return tuple(dict(item) for item in raw_clips)
|
||||
|
||||
|
||||
def _source_provenance(root: Path, frames_path: Path) -> dict[str, str]:
|
||||
return {
|
||||
"result_id": root.name,
|
||||
"manifest_sha256": _file_sha256(root / "manifest.json"),
|
||||
"frames_sha256": _file_sha256(frames_path),
|
||||
}
|
||||
|
||||
|
||||
def _validate_camera_rows(rows: tuple[dict[str, Any], ...]) -> None:
|
||||
if len(rows) != M48_FRAME_COUNT:
|
||||
raise M48Ravnoves00PackError("recorded camera index frame count changed")
|
||||
previous_session_time = -1
|
||||
for expected_sequence, row in enumerate(rows, start=1):
|
||||
session_time = row.get("session_monotonic_ns")
|
||||
if (
|
||||
row.get("schema_version") != _CAMERA_INDEX_SCHEMA
|
||||
or row.get("kind") != "media"
|
||||
or row.get("sequence") != expected_sequence
|
||||
or not isinstance(session_time, int)
|
||||
or session_time <= previous_session_time
|
||||
or not _is_sha256(row.get("sha256"))
|
||||
):
|
||||
raise M48Ravnoves00PackError("recorded camera index changed")
|
||||
previous_session_time = session_time
|
||||
|
||||
|
||||
def _validate_bound_frame(
|
||||
*,
|
||||
graph_row: Mapping[str, Any],
|
||||
threat_row: Mapping[str, Any],
|
||||
geometry_row: Mapping[str, Any],
|
||||
camera_row: Mapping[str, Any],
|
||||
frame_index: int,
|
||||
previous_source_time_ns: int,
|
||||
) -> int:
|
||||
obstacle_map = _mapping(graph_row.get("obstacle_map"), "M4.7 obstacle map")
|
||||
source_time_ns = threat_row.get("source_time_ns")
|
||||
if (
|
||||
graph_row.get("sequence") != frame_index
|
||||
or obstacle_map.get("schema_version") != _GRAPH_FRAME_SCHEMA
|
||||
or obstacle_map.get("frame_id") != f"frame-{frame_index:06d}"
|
||||
or not isinstance(obstacle_map.get("free_space_claimed"), bool)
|
||||
or threat_row.get("schema_version") not in _THREAT_FRAME_SCHEMAS
|
||||
or threat_row.get("sequence") != frame_index
|
||||
or threat_row.get("frame_id") != f"frame-{frame_index:06d}"
|
||||
or geometry_row.get("schema_version") != "missioncore.perception-geometry-replay-frame/v1"
|
||||
or geometry_row.get("sequence") != frame_index
|
||||
or geometry_row.get("frame_id") != f"frame-{frame_index:06d}"
|
||||
or geometry_row.get("source_available") != threat_row.get("source_available")
|
||||
or not isinstance(source_time_ns, int)
|
||||
or source_time_ns <= previous_source_time_ns
|
||||
or camera_row.get("sequence") != frame_index + 1
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.8 frame binding changed")
|
||||
return source_time_ns
|
||||
|
||||
|
||||
def _prediction_objects(
|
||||
value: object,
|
||||
*,
|
||||
geometry_observations: object,
|
||||
metric_obstacles: object,
|
||||
) -> list[dict[str, object]]:
|
||||
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
||||
raise M48Ravnoves00PackError("M4.6 camera proposal collection changed")
|
||||
observations = _proposal_observations(geometry_observations)
|
||||
obstacles = _metric_obstacles(metric_obstacles)
|
||||
objects: list[dict[str, object]] = []
|
||||
seen: set[str] = set()
|
||||
for proposal in value:
|
||||
prediction_id = proposal.get("proposal_id")
|
||||
occupied_support = proposal.get("occupied_support")
|
||||
threat_value = proposal.get("threat_decision")
|
||||
if (
|
||||
not isinstance(prediction_id, str)
|
||||
or prediction_id in seen
|
||||
or not isinstance(occupied_support, bool)
|
||||
or threat_value not in {None, "threat", "not-threat", "unknown"}
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.6 camera proposal identity changed")
|
||||
seen.add(prediction_id)
|
||||
observation = observations.get(prediction_id)
|
||||
geometry = "associated" if occupied_support else "unknown"
|
||||
freshness = "current"
|
||||
motion = "unsupported"
|
||||
threat = threat_value if isinstance(threat_value, str) else "unknown"
|
||||
if occupied_support:
|
||||
if observation is None:
|
||||
raise M48Ravnoves00PackError("associated proposal lost its geometry observation")
|
||||
currentness = observation.get("currentness")
|
||||
if currentness not in {"current", "held", "stale", "unavailable"}:
|
||||
raise M48Ravnoves00PackError("associated proposal currentness changed")
|
||||
freshness = str(currentness)
|
||||
centroid = _metric_centroid(observation)
|
||||
obstacle = _match_metric_obstacle(centroid, obstacles)
|
||||
raw_motion = obstacle.get("motion")
|
||||
motion = {
|
||||
"moving": "moving",
|
||||
"stationary": "static",
|
||||
"unknown": "unknown",
|
||||
}.get(str(raw_motion), "")
|
||||
assessment = _mapping(obstacle.get("assessment"), "metric obstacle assessment")
|
||||
obstacle_threat = assessment.get("decision")
|
||||
if not motion or obstacle_threat not in {"threat", "not-threat", "unknown"}:
|
||||
raise M48Ravnoves00PackError("associated proposal state changed")
|
||||
threat = str(obstacle_threat)
|
||||
causes: set[str] = set()
|
||||
if geometry == "unknown":
|
||||
causes.add("insufficient-geometry-support")
|
||||
if threat == "unknown":
|
||||
causes.add("threat-evidence-insufficient")
|
||||
if motion == "unknown":
|
||||
causes.add("motion-not-supported")
|
||||
objects.append(
|
||||
{
|
||||
"prediction_id": prediction_id,
|
||||
"extent_xyxy": _normalized_extent(proposal.get("bbox_xyxy")),
|
||||
"geometry_association": geometry,
|
||||
"freshness": freshness,
|
||||
"motion": motion,
|
||||
"threat": threat,
|
||||
"unknown_causes": sorted(causes),
|
||||
}
|
||||
)
|
||||
return objects
|
||||
|
||||
|
||||
def _proposal_observations(value: object) -> dict[str, dict[str, Any]]:
|
||||
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
||||
raise M48Ravnoves00PackError("M4.4 observation collection changed")
|
||||
mapped: dict[str, dict[str, Any]] = {}
|
||||
for observation in value:
|
||||
proposal_ids = observation.get("proposal_ids")
|
||||
if not isinstance(proposal_ids, list) or any(
|
||||
not isinstance(item, str) for item in proposal_ids
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.4 proposal binding changed")
|
||||
for proposal_id in proposal_ids:
|
||||
if proposal_id in mapped:
|
||||
raise M48Ravnoves00PackError("M4.4 proposal has multiple observations")
|
||||
mapped[proposal_id] = observation
|
||||
return mapped
|
||||
|
||||
|
||||
def _metric_obstacles(value: object) -> tuple[dict[str, Any], ...]:
|
||||
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
||||
raise M48Ravnoves00PackError("M4.6 metric obstacle collection changed")
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def _metric_centroid(observation: Mapping[str, Any]) -> tuple[float, float, float]:
|
||||
geometry = _mapping(observation.get("metric_geometry"), "proposal metric geometry")
|
||||
value = geometry.get("centroid_xyz_m")
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 3
|
||||
or any(
|
||||
not isinstance(item, (int, float))
|
||||
or isinstance(item, bool)
|
||||
or not math.isfinite(float(item))
|
||||
for item in value
|
||||
)
|
||||
):
|
||||
raise M48Ravnoves00PackError("proposal metric centroid changed")
|
||||
return float(value[0]), float(value[1]), float(value[2])
|
||||
|
||||
|
||||
def _match_metric_obstacle(
|
||||
centroid: tuple[float, float, float],
|
||||
obstacles: tuple[dict[str, Any], ...],
|
||||
) -> dict[str, Any]:
|
||||
matches: list[dict[str, Any]] = []
|
||||
for obstacle in obstacles:
|
||||
value = obstacle.get("centroid_map_xyz_m")
|
||||
if (
|
||||
isinstance(value, list)
|
||||
and len(value) == 3
|
||||
and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in value)
|
||||
and max(
|
||||
abs(float(left) - float(right)) for left, right in zip(value, centroid, strict=True)
|
||||
)
|
||||
<= 1e-9
|
||||
):
|
||||
matches.append(obstacle)
|
||||
if len(matches) != 1:
|
||||
raise M48Ravnoves00PackError("proposal metric obstacle association is ambiguous")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _normalized_extent(value: object) -> list[float]:
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 4
|
||||
or any(
|
||||
not isinstance(item, (int, float))
|
||||
or isinstance(item, bool)
|
||||
or not math.isfinite(float(item))
|
||||
for item in value
|
||||
)
|
||||
):
|
||||
raise M48Ravnoves00PackError("M4.6 proposal extent changed")
|
||||
x_min, y_min, x_max, y_max = (float(item) for item in value)
|
||||
extent = [
|
||||
x_min / M48_IMAGE_WIDTH,
|
||||
y_min / M48_IMAGE_HEIGHT,
|
||||
x_max / M48_IMAGE_WIDTH,
|
||||
y_max / M48_IMAGE_HEIGHT,
|
||||
]
|
||||
if not 0.0 <= extent[0] < extent[2] <= 1.0 or not 0.0 <= extent[1] < extent[3] <= 1.0:
|
||||
raise M48Ravnoves00PackError("M4.6 proposal extent escaped the camera raster")
|
||||
return extent
|
||||
|
||||
|
||||
def _iter_jsonl(path: Path, label: str) -> Iterator[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line_number, line in enumerate(stream, start=1):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise M48Ravnoves00PackError(f"{label} row {line_number} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise M48Ravnoves00PackError(f"{label} row {line_number} is not an object")
|
||||
yield value
|
||||
|
||||
|
||||
def _read_json(path: Path, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise M48Ravnoves00PackError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise M48Ravnoves00PackError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _mapping(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise M48Ravnoves00PackError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise M48Ravnoves00PackError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M48Ravnoves00PackError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M48Ravnoves00PackError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_dir():
|
||||
raise M48Ravnoves00PackError(f"{label} is unavailable")
|
||||
return resolved
|
||||
|
||||
|
||||
def _file(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M48Ravnoves00PackError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M48Ravnoves00PackError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_file():
|
||||
raise M48Ravnoves00PackError(f"{label} is unavailable")
|
||||
return resolved
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _is_sha256(value: object) -> bool:
|
||||
return isinstance(value, str) and _SHA256.fullmatch(value) is not None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48Ravnoves00PackError",
|
||||
"M48_SELECTION_ID",
|
||||
"M48_SELECTION_SCHEMA",
|
||||
"prepare_m48_ravnoves00_pack",
|
||||
]
|
||||
@@ -0,0 +1,507 @@
|
||||
"""Prediction-free raw spatial evidence for the neutral M4.8 review surface.
|
||||
|
||||
This reader deliberately does not open the M4.7 graph payload or the frozen M4.8
|
||||
prediction ledger. It reuses the already verified recorded-geometry and replay
|
||||
body-frame primitives to expose only a bounded current LiDAR increment in the
|
||||
virtual body frame, together with immutable rig/corridor parameters.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.laboratory.m47_reference_graph import (
|
||||
M47ReferenceGraphLabError,
|
||||
read_m47_reference_graph_lab,
|
||||
)
|
||||
from k1link.laboratory.m48_object_quality import M48ObjectQualityPack
|
||||
from k1link.perception.spatial_evidence import (
|
||||
SpatialEvidenceProjectionError,
|
||||
sample_points_in_body_frame,
|
||||
)
|
||||
from k1link.perception.threat_replay import (
|
||||
ThreatReplayError,
|
||||
ThreatReplayResult,
|
||||
read_threat_replay_result,
|
||||
)
|
||||
from k1link.perception.threat_timeline import (
|
||||
RECORDED_SPATIAL_POINT_LIMIT,
|
||||
RecordedThreatTimeline,
|
||||
RecordedThreatTimelineError,
|
||||
)
|
||||
|
||||
M48_RAW_SPATIAL_FRAME_SCHEMA: Final = "missioncore.m48-neutral-object-review-spatial-frame/v1"
|
||||
M48_EXPECTED_SOURCE_ID: Final = "RAVNOVES00"
|
||||
M48_EXPECTED_E10_SOURCE_ID: Final = "sensor.camera.right"
|
||||
M48_EXPECTED_SESSION_ID: Final = "20260720T065719Z_viewer_live"
|
||||
M48_EXPECTED_FRAME_COUNT: Final = 4_489
|
||||
M48_EXPECTED_SOURCE_PACK_ID: Final = (
|
||||
"e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
||||
)
|
||||
M48_EXPECTED_SOURCE_PACK_SHA256: Final = (
|
||||
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
|
||||
)
|
||||
M48_EXPECTED_THREAT_RESULT_ID: Final = (
|
||||
"m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324"
|
||||
)
|
||||
|
||||
_E10_SCHEMA: Final = "missioncore.e10-lidar-replay-pack/v1"
|
||||
_E10_ARTIFACT_NAME: Final = "lidar-pack.npz"
|
||||
_FALSE_AUTHORITY: Final = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
_MANIFEST_KEYS: Final = {
|
||||
"artifact",
|
||||
"classification",
|
||||
"created_at_utc",
|
||||
"ground_truth",
|
||||
"identity",
|
||||
"identity_sha256",
|
||||
"pack_id",
|
||||
"schema_version",
|
||||
}
|
||||
_IDENTITY_KEYS: Final = {
|
||||
"available_lidar_frames",
|
||||
"calibration_sha256",
|
||||
"camera_slot",
|
||||
"e6_profile_sha256",
|
||||
"e6_result_id",
|
||||
"frame_count",
|
||||
"input_sha256",
|
||||
"job_id",
|
||||
"point_count",
|
||||
"producer_sha256",
|
||||
"projection",
|
||||
"schema_version",
|
||||
"semantic_timeline_result_id",
|
||||
"session_id",
|
||||
"source_end_frame_index",
|
||||
"source_id",
|
||||
"source_start_frame_index",
|
||||
"temporal_binding",
|
||||
"temporal_policy",
|
||||
"timeline_end_seconds",
|
||||
"timeline_start_seconds",
|
||||
}
|
||||
_ARTIFACT_KEYS: Final = {"byte_length", "media_type", "path", "sha256"}
|
||||
|
||||
|
||||
class M48RawEvidenceError(RuntimeError):
|
||||
"""Neutral M4.8 spatial evidence escaped an immutable source binding."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _PackFrameBinding:
|
||||
clip_id: str
|
||||
source_time_ns: int
|
||||
|
||||
|
||||
class M48RawEvidenceReader:
|
||||
"""Provide one prediction-blind, bounded body-frame projection per call.
|
||||
|
||||
Construct production instances with :meth:`from_repository`. The object is
|
||||
directly compatible with the M4.8 API provider callable:
|
||||
``reader(pack, one_based_sequence)``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
repository_root: Path,
|
||||
threat_result: ThreatReplayResult,
|
||||
timeline: RecordedThreatTimeline,
|
||||
point_limit: int = RECORDED_SPATIAL_POINT_LIMIT,
|
||||
) -> None:
|
||||
if (
|
||||
not isinstance(point_limit, int)
|
||||
or isinstance(point_limit, bool)
|
||||
or not 1 <= point_limit <= RECORDED_SPATIAL_POINT_LIMIT
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 raw evidence point limit is invalid")
|
||||
self.repository_root = repository_root.resolve(strict=True)
|
||||
self.threat_result = threat_result
|
||||
self.timeline = timeline
|
||||
self.point_limit = point_limit
|
||||
self._pack_indices: dict[str, dict[int, _PackFrameBinding]] = {}
|
||||
self._lock = RLock()
|
||||
|
||||
@classmethod
|
||||
def from_repository(
|
||||
cls,
|
||||
*,
|
||||
repository_root: Path,
|
||||
threat_result_root: Path,
|
||||
expected_source_pack_id: str = M48_EXPECTED_SOURCE_PACK_ID,
|
||||
point_limit: int = RECORDED_SPATIAL_POINT_LIMIT,
|
||||
) -> M48RawEvidenceReader:
|
||||
"""Open the exact sealed M4 result and its exact E10 source generation.
|
||||
|
||||
``threat_result_root`` is the immutable result generation directory, not
|
||||
the parent collection. No latest-by-mtime discovery is permitted.
|
||||
"""
|
||||
|
||||
repository = _strict_directory(repository_root, "repository root")
|
||||
if expected_source_pack_id != M48_EXPECTED_SOURCE_PACK_ID:
|
||||
raise M48RawEvidenceError("M4.8 E10 pack id escaped the canonical binding")
|
||||
threat_root = _strict_directory(threat_result_root, "threat result root")
|
||||
try:
|
||||
result = read_threat_replay_result(threat_root)
|
||||
except (OSError, ValueError, ThreatReplayError) as exc:
|
||||
raise M48RawEvidenceError("M4.8 threat result is invalid") from exc
|
||||
_validate_threat_result(result, expected_source_pack_id=expected_source_pack_id)
|
||||
pack_root = (
|
||||
repository / ".runtime/compute-experiments/e10/lidar-packs" / expected_source_pack_id
|
||||
)
|
||||
_validate_e10_pack(
|
||||
pack_root,
|
||||
expected_pack_id=expected_source_pack_id,
|
||||
expected_artifact_sha256=M48_EXPECTED_SOURCE_PACK_SHA256,
|
||||
)
|
||||
try:
|
||||
timeline = RecordedThreatTimeline(repository_root=repository, result=result)
|
||||
except (OSError, ValueError, RecordedThreatTimelineError) as exc:
|
||||
raise M48RawEvidenceError("M4.8 recorded geometry timeline is invalid") from exc
|
||||
if (
|
||||
len(timeline.index.source_times_ns) != M48_EXPECTED_FRAME_COUNT
|
||||
or timeline.profile.source_id != M48_EXPECTED_SOURCE_ID
|
||||
or timeline.profile.session_id != M48_EXPECTED_SESSION_ID
|
||||
or timeline.profile.source_pack_id != expected_source_pack_id
|
||||
or timeline.profile.source_pack_sha256 != M48_EXPECTED_SOURCE_PACK_SHA256
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 recorded geometry binding changed")
|
||||
return cls(
|
||||
repository_root=repository,
|
||||
threat_result=result,
|
||||
timeline=timeline,
|
||||
point_limit=point_limit,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
pack: M48ObjectQualityPack,
|
||||
sequence: int,
|
||||
) -> dict[str, object]:
|
||||
return self.frame(pack=pack, sequence=sequence)
|
||||
|
||||
def frame(
|
||||
self,
|
||||
*,
|
||||
pack: M48ObjectQualityPack,
|
||||
sequence: int,
|
||||
) -> dict[str, object]:
|
||||
"""Return one one-based, clip-bound neutral spatial frame."""
|
||||
|
||||
if (
|
||||
not isinstance(sequence, int)
|
||||
or isinstance(sequence, bool)
|
||||
or not 1 <= sequence <= M48_EXPECTED_FRAME_COUNT
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 raw evidence sequence is invalid")
|
||||
binding = self._binding_for(pack, sequence)
|
||||
frame_index = sequence - 1
|
||||
try:
|
||||
temporal = self.timeline.store.temporal_binding_for_index(frame_index)
|
||||
body_frame = self.timeline.body_frames.body_frame_for_frame(f"frame-{frame_index:06d}")
|
||||
except (RuntimeError, TypeError, ValueError) as exc:
|
||||
raise M48RawEvidenceError("M4.8 source frame binding is invalid") from exc
|
||||
if temporal.frame_index != frame_index or temporal.source_time_ns != binding.source_time_ns:
|
||||
raise M48RawEvidenceError("M4.8 source time escaped the neutral frame reference")
|
||||
|
||||
points_body: list[list[float]] = []
|
||||
if body_frame is not None:
|
||||
if not temporal.source_available:
|
||||
raise M48RawEvidenceError("unavailable source produced an M4.8 body frame")
|
||||
points = self.timeline.store.current_points_for_frame(frame_index)
|
||||
if points is None:
|
||||
raise M48RawEvidenceError("qualified M4.8 body frame lacks current LiDAR")
|
||||
try:
|
||||
points_body, _ = sample_points_in_body_frame(
|
||||
points,
|
||||
body_frame,
|
||||
point_limit=self.point_limit,
|
||||
)
|
||||
except SpatialEvidenceProjectionError as exc:
|
||||
raise M48RawEvidenceError("M4.8 body-frame point projection failed") from exc
|
||||
|
||||
profile = self.timeline.profile
|
||||
return {
|
||||
"schema_version": M48_RAW_SPATIAL_FRAME_SCHEMA,
|
||||
"pack_id": pack.result_id,
|
||||
"clip_id": binding.clip_id,
|
||||
"sequence": sequence,
|
||||
"source_time_ns": temporal.source_time_ns,
|
||||
"source_available": temporal.source_available,
|
||||
"body_frame_available": body_frame is not None,
|
||||
"point_cloud_body_xyz_m": points_body,
|
||||
"rig": {
|
||||
"profile_id": profile.rig.profile_id,
|
||||
"length_m": profile.rig.body_length_m,
|
||||
"width_m": profile.rig.body_width_m,
|
||||
"lidar_reference": profile.rig.lidar_reference,
|
||||
"nominal_sensor_height_m": profile.rig.nominal_sensor_height_m,
|
||||
"physical_mount_claimed": False,
|
||||
},
|
||||
"corridor": {
|
||||
"profile_id": profile.corridor.profile_id,
|
||||
"forward_length_m": profile.corridor.forward_length_m,
|
||||
"rear_margin_m": profile.corridor.rear_margin_m,
|
||||
"lateral_clearance_m": profile.corridor.lateral_clearance_m,
|
||||
"half_width_m": (
|
||||
profile.rig.body_width_m / 2 + profile.corridor.lateral_clearance_m
|
||||
),
|
||||
"prediction_horizon_seconds": (profile.corridor.prediction_horizon_seconds),
|
||||
},
|
||||
"occupied_voxel_size_m": profile.corridor.occupied_voxel_size_m,
|
||||
"candidate_identity_included": False,
|
||||
"graph_boxes_ids_scores_included": False,
|
||||
"frozen_predictions_included": False,
|
||||
"strata_included": False,
|
||||
"authority": dict(_FALSE_AUTHORITY),
|
||||
}
|
||||
|
||||
def _binding_for(
|
||||
self,
|
||||
pack: M48ObjectQualityPack,
|
||||
sequence: int,
|
||||
) -> _PackFrameBinding:
|
||||
with self._lock:
|
||||
index = self._pack_indices.get(pack.result_id)
|
||||
if index is None:
|
||||
_validate_m47_pack_binding(
|
||||
repository_root=self.repository_root,
|
||||
pack=pack,
|
||||
threat_result=self.threat_result,
|
||||
)
|
||||
index = _index_neutral_frame_references(pack)
|
||||
self._pack_indices[pack.result_id] = index
|
||||
binding = index.get(sequence)
|
||||
if binding is None:
|
||||
raise M48RawEvidenceError("M4.8 sequence is outside the selected neutral clips")
|
||||
return binding
|
||||
|
||||
|
||||
def _validate_threat_result(
|
||||
result: ThreatReplayResult,
|
||||
*,
|
||||
expected_source_pack_id: str,
|
||||
) -> None:
|
||||
identity = result.manifest.get("identity")
|
||||
metrics = identity.get("metrics") if isinstance(identity, dict) else None
|
||||
frames = metrics.get("frames") if isinstance(metrics, dict) else None
|
||||
if (
|
||||
result.result_id != M48_EXPECTED_THREAT_RESULT_ID
|
||||
or result.result_root.name != result.result_id
|
||||
or result.accepted is not True
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("source_id") != M48_EXPECTED_SOURCE_ID
|
||||
or identity.get("source_session_id") != M48_EXPECTED_SESSION_ID
|
||||
or identity.get("source_pack_id") != expected_source_pack_id
|
||||
or identity.get("source_pack_sha256") != M48_EXPECTED_SOURCE_PACK_SHA256
|
||||
or not isinstance(frames, dict)
|
||||
or frames.get("total") != M48_EXPECTED_FRAME_COUNT
|
||||
or identity.get("authority")
|
||||
!= {
|
||||
**_FALSE_AUTHORITY,
|
||||
"physical_collision_accepted": False,
|
||||
"ground_truth": False,
|
||||
}
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 threat result escaped the canonical source")
|
||||
|
||||
|
||||
def _validate_e10_pack(
|
||||
pack_root: Path,
|
||||
*,
|
||||
expected_pack_id: str,
|
||||
expected_artifact_sha256: str,
|
||||
) -> Path:
|
||||
root = _strict_directory(pack_root, "E10 pack root")
|
||||
if root.name != expected_pack_id:
|
||||
raise M48RawEvidenceError("E10 pack path escaped its expected identity")
|
||||
manifest_path = root / "manifest.json"
|
||||
if (
|
||||
manifest_path.is_symlink()
|
||||
or not manifest_path.is_file()
|
||||
or manifest_path.resolve(strict=True).parent != root
|
||||
):
|
||||
raise M48RawEvidenceError("E10 manifest path is invalid")
|
||||
manifest = _read_json(manifest_path, "E10 manifest")
|
||||
if set(manifest) != _MANIFEST_KEYS:
|
||||
raise M48RawEvidenceError("E10 manifest fields changed")
|
||||
identity = _mapping(manifest.get("identity"), "E10 identity")
|
||||
artifact = _mapping(manifest.get("artifact"), "E10 artifact")
|
||||
if set(identity) != _IDENTITY_KEYS or set(artifact) != _ARTIFACT_KEYS:
|
||||
raise M48RawEvidenceError("E10 identity or artifact fields changed")
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
if (
|
||||
manifest.get("schema_version") != _E10_SCHEMA
|
||||
or manifest.get("pack_id") != expected_pack_id
|
||||
or manifest.get("identity_sha256") != identity_sha256
|
||||
or expected_pack_id != f"e10-lidar-pack-{identity_sha256}"
|
||||
or manifest.get("classification") != "private-recorded-sensor-replay-input"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or identity.get("schema_version") != _E10_SCHEMA
|
||||
or identity.get("source_id") != M48_EXPECTED_E10_SOURCE_ID
|
||||
or identity.get("session_id") != M48_EXPECTED_SESSION_ID
|
||||
or identity.get("frame_count") != M48_EXPECTED_FRAME_COUNT
|
||||
or identity.get("source_start_frame_index") != 0
|
||||
or identity.get("source_end_frame_index") != M48_EXPECTED_FRAME_COUNT - 1
|
||||
or artifact.get("path") != _E10_ARTIFACT_NAME
|
||||
or artifact.get("media_type") != "application/x-npz"
|
||||
or artifact.get("sha256") != expected_artifact_sha256
|
||||
):
|
||||
raise M48RawEvidenceError("E10 pack identity changed")
|
||||
byte_length = artifact.get("byte_length")
|
||||
if not isinstance(byte_length, int) or isinstance(byte_length, bool) or byte_length < 1:
|
||||
raise M48RawEvidenceError("E10 artifact byte length is invalid")
|
||||
artifact_path = root / _E10_ARTIFACT_NAME
|
||||
if (
|
||||
artifact_path.is_symlink()
|
||||
or not artifact_path.is_file()
|
||||
or artifact_path.resolve(strict=True).parent != root
|
||||
or artifact_path.stat().st_size != byte_length
|
||||
or _file_sha256(artifact_path) != expected_artifact_sha256
|
||||
):
|
||||
raise M48RawEvidenceError("E10 artifact content changed")
|
||||
return artifact_path.resolve(strict=True)
|
||||
|
||||
|
||||
def _validate_m47_pack_binding(
|
||||
*,
|
||||
repository_root: Path,
|
||||
pack: M48ObjectQualityPack,
|
||||
threat_result: ThreatReplayResult,
|
||||
) -> None:
|
||||
identity = _mapping(pack.manifest.get("identity"), "M4.8 pack identity")
|
||||
source = _mapping(identity.get("source"), "M4.8 pack source")
|
||||
m47_id = source.get("m47_lab_result_id")
|
||||
m47_manifest_sha256 = source.get("m47_lab_manifest_sha256")
|
||||
if (
|
||||
source.get("source_id") != M48_EXPECTED_SOURCE_ID
|
||||
or source.get("source_session_id") != M48_EXPECTED_SESSION_ID
|
||||
or not isinstance(m47_id, str)
|
||||
or not isinstance(m47_manifest_sha256, str)
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 pack source binding changed")
|
||||
m47_root = repository_root / ".runtime/compute-experiments/m47/reference-graph-labs" / m47_id
|
||||
manifest_path = m47_root / "manifest.json"
|
||||
if (
|
||||
manifest_path.is_symlink()
|
||||
or not manifest_path.is_file()
|
||||
or _file_sha256(manifest_path) != m47_manifest_sha256
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 pack M4.7 manifest binding changed")
|
||||
try:
|
||||
m47 = read_m47_reference_graph_lab(m47_root)
|
||||
except (OSError, ValueError, M47ReferenceGraphLabError) as exc:
|
||||
raise M48RawEvidenceError("M4.8 pack M4.7 LAB is invalid") from exc
|
||||
m47_source = _mapping(m47.report.get("source"), "M4.7 source")
|
||||
threat_identity = _mapping(threat_result.manifest.get("identity"), "M4 threat identity")
|
||||
if (
|
||||
m47.manifest.get("accepted") is not True
|
||||
or m47_source.get("source_id") != M48_EXPECTED_SOURCE_ID
|
||||
or m47_source.get("source_session_id") != M48_EXPECTED_SESSION_ID
|
||||
or m47_source.get("visual_result_id") != threat_result.result_id
|
||||
or m47_source.get("threat_frames_sha256") != threat_identity.get("frames_sha256")
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 pack escaped its accepted M4.7 visual source")
|
||||
|
||||
|
||||
def _index_neutral_frame_references(
|
||||
pack: M48ObjectQualityPack,
|
||||
) -> dict[int, _PackFrameBinding]:
|
||||
index: dict[int, _PackFrameBinding] = {}
|
||||
for raw in pack.frame_references:
|
||||
row = _mapping(raw, "M4.8 neutral frame reference")
|
||||
sequence = row.get("sequence")
|
||||
source_time_ns = row.get("source_time_ns")
|
||||
clip_id = row.get("clip_id")
|
||||
if (
|
||||
not isinstance(sequence, int)
|
||||
or isinstance(sequence, bool)
|
||||
or not 1 <= sequence <= M48_EXPECTED_FRAME_COUNT
|
||||
or not isinstance(source_time_ns, int)
|
||||
or isinstance(source_time_ns, bool)
|
||||
or source_time_ns < 0
|
||||
or not isinstance(clip_id, str)
|
||||
or not clip_id
|
||||
or sequence in index
|
||||
):
|
||||
raise M48RawEvidenceError("M4.8 neutral frame references are invalid")
|
||||
index[sequence] = _PackFrameBinding(
|
||||
clip_id=clip_id,
|
||||
source_time_ns=source_time_ns,
|
||||
)
|
||||
if not index:
|
||||
raise M48RawEvidenceError("M4.8 neutral frame reference set is empty")
|
||||
return index
|
||||
|
||||
|
||||
def _strict_directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M48RawEvidenceError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M48RawEvidenceError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_dir():
|
||||
raise M48RawEvidenceError(f"{label} is not a directory")
|
||||
return resolved
|
||||
|
||||
|
||||
def _read_json(path: Path, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise M48RawEvidenceError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise M48RawEvidenceError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _mapping(value: object, label: str) -> Mapping[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise M48RawEvidenceError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
return hashlib.sha256(
|
||||
json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
while chunk := handle.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48_EXPECTED_FRAME_COUNT",
|
||||
"M48_EXPECTED_SESSION_ID",
|
||||
"M48_EXPECTED_SOURCE_ID",
|
||||
"M48_EXPECTED_SOURCE_PACK_ID",
|
||||
"M48_EXPECTED_THREAT_RESULT_ID",
|
||||
"M48_RAW_SPATIAL_FRAME_SCHEMA",
|
||||
"M48RawEvidenceError",
|
||||
"M48RawEvidenceReader",
|
||||
]
|
||||
@@ -0,0 +1,711 @@
|
||||
"""Immutable M4.8 development regression over operator-added missed-object anchors.
|
||||
|
||||
The experiment deliberately stays inside M4.8 and reuses the frozen Worker 006
|
||||
prediction pack. It snapshots only operator-added tracklets from reviewed clips,
|
||||
compares the exact source frame against the already-frozen prediction row, and
|
||||
publishes a separate append-only result. The assisted correction is never called
|
||||
independent truth and the result grants no navigation or safety authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.laboratory.m48_object_quality import (
|
||||
M48ObjectQualityError,
|
||||
read_m48_object_quality_pack,
|
||||
)
|
||||
|
||||
M48_SMALL_STATIC_PROFILE_SCHEMA: Final = (
|
||||
"missioncore.m48-small-static-passage-regression-profile/v1"
|
||||
)
|
||||
M48_SMALL_STATIC_RESULT_SCHEMA: Final = (
|
||||
"missioncore.m48-small-static-passage-regression-result/v1"
|
||||
)
|
||||
M48_SMALL_STATIC_REPORT_SCHEMA: Final = (
|
||||
"missioncore.m48-small-static-passage-regression-report/v1"
|
||||
)
|
||||
M48_SMALL_STATIC_ANCHOR_SCHEMA: Final = (
|
||||
"missioncore.m48-assisted-missed-object-anchor/v1"
|
||||
)
|
||||
M48_SMALL_STATIC_COMPARISON_SCHEMA: Final = (
|
||||
"missioncore.m48-assisted-anchor-comparison/v1"
|
||||
)
|
||||
M48_SMALL_STATIC_PREFIX: Final = "m48-small-static-passage-regression-"
|
||||
|
||||
_CORRECTION_SCHEMA: Final = "missioncore.m48-assisted-object-correction-session/v1"
|
||||
_METHOD_SCHEMA: Final = "missioncore.laboratory-method/v1"
|
||||
_OBJECT_ID = re.compile(r"^object-[0-9]{2,}$")
|
||||
_AUTHORITY: Final = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class M48SmallStaticRegressionError(RuntimeError):
|
||||
"""The assisted development-regression source or result is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48SmallStaticRegressionResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
anchors: tuple[dict[str, Any], ...]
|
||||
comparisons: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
def build_m48_small_static_passage_regression(
|
||||
*,
|
||||
pack_root: Path,
|
||||
correction_session_path: Path,
|
||||
profile_path: Path,
|
||||
output_root: Path,
|
||||
run_created_at_utc: str | None = None,
|
||||
) -> M48SmallStaticRegressionResult:
|
||||
"""Publish one append-only M4.8R development baseline without mutating inputs."""
|
||||
|
||||
try:
|
||||
pack = read_m48_object_quality_pack(pack_root)
|
||||
except M48ObjectQualityError as exc:
|
||||
raise M48SmallStaticRegressionError("M4.8 frozen prediction pack is invalid") from exc
|
||||
profile_bytes, profile = _read_profile(profile_path)
|
||||
correction_bytes, correction = _read_correction(correction_session_path, pack.result_id)
|
||||
anchors = _assisted_anchors(correction)
|
||||
if len(anchors) < int(profile["minimum_anchor_count"]):
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted anchor set is too small")
|
||||
|
||||
prediction_rows: dict[tuple[str, int], dict[str, Any]] = {}
|
||||
for row in pack.predictions:
|
||||
clip_id = row.get("clip_id")
|
||||
sequence = row.get("sequence")
|
||||
if not isinstance(clip_id, str) or not _integer(sequence):
|
||||
raise M48SmallStaticRegressionError("M4.8 frozen prediction binding is invalid")
|
||||
key = (clip_id, int(sequence))
|
||||
if key in prediction_rows:
|
||||
raise M48SmallStaticRegressionError("M4.8 frozen prediction binding collided")
|
||||
prediction_rows[key] = row
|
||||
|
||||
threshold = float(profile["extent_iou_threshold"])
|
||||
comparisons = tuple(
|
||||
_compare_anchor(anchor, prediction_rows, threshold)
|
||||
for anchor in anchors
|
||||
)
|
||||
recalled = sum(bool(row["matched_at_threshold"]) for row in comparisons)
|
||||
recall = recalled / len(comparisons)
|
||||
passage_count = sum(bool(row["requires_avoidance_or_clearance"]) for row in anchors)
|
||||
clip_count = len({str(row["clip_id"]) for row in anchors})
|
||||
target = float(profile["minimum_assisted_anchor_recall"])
|
||||
accepted = recall >= target
|
||||
created_at = _utc_timestamp(run_created_at_utc or datetime.now(UTC).isoformat())
|
||||
correction_sha256 = hashlib.sha256(correction_bytes).hexdigest()
|
||||
profile_sha256 = hashlib.sha256(profile_bytes).hexdigest()
|
||||
producer_sha256 = _file_sha256(Path(__file__).resolve())
|
||||
pack_identity = _object(pack.manifest.get("identity"), "M4.8 pack identity")
|
||||
freeze = _object(pack_identity.get("freeze"), "M4.8 pack freeze")
|
||||
|
||||
identity: dict[str, Any] = {
|
||||
"schema_version": M48_SMALL_STATIC_RESULT_SCHEMA,
|
||||
"human_lab_id": profile["human_lab_id"],
|
||||
"run_label": profile["run_label"],
|
||||
"run_created_at_utc": created_at,
|
||||
"pipeline_id": profile["pipeline_id"],
|
||||
"experiment_id": profile["experiment_id"],
|
||||
"profile_id": profile["profile_id"],
|
||||
"profile_sha256": profile_sha256,
|
||||
"producer_sha256": producer_sha256,
|
||||
"source": {
|
||||
"source_id": _object(
|
||||
pack_identity.get("source"), "M4.8 source"
|
||||
).get("source_id"),
|
||||
"source_session_id": _object(
|
||||
pack_identity.get("source"), "M4.8 source"
|
||||
).get("source_session_id"),
|
||||
"pack_id": pack.result_id,
|
||||
"pack_identity_sha256": pack.manifest["identity_sha256"],
|
||||
"prediction_rows_sha256": freeze.get("prediction_rows_sha256"),
|
||||
"correction_session_id": correction["session_id"],
|
||||
"correction_revision": correction["revision"],
|
||||
"correction_updated_at_utc": correction["updated_at_utc"],
|
||||
"correction_document_sha256": correction_sha256,
|
||||
"correction_independent_truth": False,
|
||||
},
|
||||
"selection": {
|
||||
"anchor_selection": profile["anchor_selection"],
|
||||
"assisted_tracklet_count": len({(row["clip_id"], row["object_id"]) for row in anchors}),
|
||||
"anchor_count": len(anchors),
|
||||
"clip_count": clip_count,
|
||||
"requires_avoidance_or_clearance_count": passage_count,
|
||||
},
|
||||
"authority": dict(_AUTHORITY),
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"{M48_SMALL_STATIC_PREFIX}{identity_sha256}"
|
||||
method = {
|
||||
"schema_version": _METHOD_SCHEMA,
|
||||
"completeness": "complete",
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": profile["pipeline_id"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "M4.8 frozen Worker 006 predictions",
|
||||
"version": pack.result_id,
|
||||
"role": "immutable candidate rows from the current M4.8 pipeline",
|
||||
"identity_sha256": freeze.get("prediction_rows_sha256"),
|
||||
},
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "operator-added missed-object anchors",
|
||||
"version": f"{correction['session_id']}:revision-{correction['revision']}",
|
||||
"role": "assisted development regression seed; not independent truth",
|
||||
"identity_sha256": correction_sha256,
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "exact-frame class-free IoU comparator",
|
||||
"version": profile["profile_id"],
|
||||
"role": "diagnostic detection recall over operator-added anchors",
|
||||
"identity_sha256": producer_sha256,
|
||||
},
|
||||
],
|
||||
}
|
||||
metrics = {
|
||||
"assisted_anchor_count": len(comparisons),
|
||||
"assisted_tracklet_count": len({(row["clip_id"], row["object_id"]) for row in anchors}),
|
||||
"anchor_clip_count": clip_count,
|
||||
"requires_avoidance_or_clearance_count": passage_count,
|
||||
"worker_recalled_anchor_count": recalled,
|
||||
"worker_missed_anchor_count": len(comparisons) - recalled,
|
||||
"assisted_anchor_recall": recall,
|
||||
"extent_iou_threshold": threshold,
|
||||
"minimum_assisted_anchor_recall": target,
|
||||
}
|
||||
gates = {
|
||||
"anchor_set_non_empty": len(comparisons) >= int(profile["minimum_anchor_count"]),
|
||||
"development_anchor_recall_target": accepted,
|
||||
"independent_truth_available": False,
|
||||
}
|
||||
decision = {
|
||||
"state": (
|
||||
"accepted-development-regression-baseline"
|
||||
if accepted
|
||||
else "failed-development-regression-baseline"
|
||||
),
|
||||
"summary": (
|
||||
f"Worker 006 matched {recalled}/{len(comparisons)} exact-frame assisted anchors "
|
||||
f"at IoU >= {threshold:.2f}."
|
||||
),
|
||||
"next_action": (
|
||||
"Keep the pipeline contract fixed, change only the perception experiment, "
|
||||
"and publish another immutable M4.8R run against this frozen seed."
|
||||
),
|
||||
}
|
||||
limitations = [
|
||||
"The anchors come from candidate-visible operator correction and are not "
|
||||
"independent truth.",
|
||||
"The seed is intentionally biased toward objects the current Worker 006 output missed.",
|
||||
"A camera rectangle is evidence of a missed visible object, not a measured 3D collider.",
|
||||
"No physical-live, navigation, command, actuation or collision-safety "
|
||||
"authority is granted.",
|
||||
]
|
||||
report = {
|
||||
"schema_version": M48_SMALL_STATIC_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"source": identity["source"],
|
||||
"configuration": {
|
||||
**profile,
|
||||
"profile_sha256": profile_sha256,
|
||||
},
|
||||
"method": method,
|
||||
"execution": {
|
||||
"comparison_node": "mission-core-local-control-plane",
|
||||
"source_worker_id": "006",
|
||||
"frozen_prediction_rows_sha256": freeze.get("prediction_rows_sha256"),
|
||||
"determinism": "exact canonical JSON + exact-frame IoU; no inference rerun",
|
||||
},
|
||||
"metrics": metrics,
|
||||
"gates": gates,
|
||||
"decision": decision,
|
||||
"limitations": limitations,
|
||||
"authority": dict(_AUTHORITY),
|
||||
"visual_review": {
|
||||
"viewer": "missioncore.laboratory-recorded-clip-viewer/v1",
|
||||
"case_count": len(comparisons),
|
||||
"camera_anchor_and_worker_boxes": True,
|
||||
"camera_3d_plan_shared_clock": True,
|
||||
},
|
||||
}
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
_publish_result(
|
||||
destination=destination,
|
||||
identity=identity,
|
||||
created_at_utc=created_at,
|
||||
accepted=accepted,
|
||||
report=report,
|
||||
anchors=anchors,
|
||||
comparisons=comparisons,
|
||||
)
|
||||
return read_m48_small_static_passage_regression(destination)
|
||||
|
||||
|
||||
def read_m48_small_static_passage_regression(
|
||||
root: Path,
|
||||
) -> M48SmallStaticRegressionResult:
|
||||
candidate = root.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M48SmallStaticRegressionError("M4.8 regression result must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression result is unavailable") from exc
|
||||
if not resolved.is_dir() or not resolved.name.startswith(M48_SMALL_STATIC_PREFIX):
|
||||
raise M48SmallStaticRegressionError("M4.8 regression result path is invalid")
|
||||
manifest = _read_json(resolved / "manifest.json", maximum=1024 * 1024)
|
||||
identity = _object(manifest.get("identity"), "M4.8 regression identity")
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
if (
|
||||
manifest.get("schema_version") != M48_SMALL_STATIC_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != resolved.name
|
||||
or manifest.get("identity_sha256") != identity_sha256
|
||||
or resolved.name != f"{M48_SMALL_STATIC_PREFIX}{identity_sha256}"
|
||||
or manifest.get("ground_truth") is not False
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 regression identity changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 3:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression artifact inventory changed")
|
||||
by_path: dict[str, dict[str, Any]] = {}
|
||||
for raw in artifacts:
|
||||
descriptor = _object(raw, "M4.8 regression artifact")
|
||||
path_name = descriptor.get("path")
|
||||
if not isinstance(path_name, str) or path_name not in {
|
||||
"anchors.jsonl", "comparisons.jsonl", "report.json"
|
||||
} or path_name in by_path:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression artifact path changed")
|
||||
path = resolved / path_name
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or descriptor.get("byte_length") != path.stat().st_size
|
||||
or descriptor.get("sha256") != _file_sha256(path)
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 regression artifact proof changed")
|
||||
by_path[path_name] = descriptor
|
||||
report = _read_json(resolved / "report.json", maximum=1024 * 1024)
|
||||
anchors = tuple(_read_jsonl(resolved / "anchors.jsonl"))
|
||||
comparisons = tuple(_read_jsonl(resolved / "comparisons.jsonl"))
|
||||
if (
|
||||
report.get("schema_version") != M48_SMALL_STATIC_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or len(anchors) != len(comparisons)
|
||||
or any(row.get("schema_version") != M48_SMALL_STATIC_ANCHOR_SCHEMA for row in anchors)
|
||||
or any(
|
||||
row.get("schema_version") != M48_SMALL_STATIC_COMPARISON_SCHEMA
|
||||
for row in comparisons
|
||||
)
|
||||
or [row.get("anchor_id") for row in anchors]
|
||||
!= [row.get("anchor_id") for row in comparisons]
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 regression content changed")
|
||||
return M48SmallStaticRegressionResult(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
anchors=anchors,
|
||||
comparisons=comparisons,
|
||||
)
|
||||
|
||||
|
||||
def _read_profile(path: Path) -> tuple[bytes, dict[str, Any]]:
|
||||
encoded, profile = _read_json_bytes(path, maximum=64 * 1024, label="M4.8 regression profile")
|
||||
expected = {
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"pipeline_id",
|
||||
"experiment_id",
|
||||
"human_lab_id",
|
||||
"run_label",
|
||||
"anchor_selection",
|
||||
"extent_iou_threshold",
|
||||
"minimum_assisted_anchor_recall",
|
||||
"minimum_anchor_count",
|
||||
"independent_truth",
|
||||
}
|
||||
if set(profile) != expected or profile.get("schema_version") != M48_SMALL_STATIC_PROFILE_SCHEMA:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression profile contract changed")
|
||||
if (
|
||||
profile.get("human_lab_id") != "M4.8"
|
||||
or profile.get("anchor_selection") != "operator-added-tracklets-in-reviewed-clips/v1"
|
||||
or profile.get("independent_truth") is not False
|
||||
or not _rate(profile.get("extent_iou_threshold"))
|
||||
or not _rate(profile.get("minimum_assisted_anchor_recall"))
|
||||
or not _integer(profile.get("minimum_anchor_count"))
|
||||
or int(profile["minimum_anchor_count"]) < 1
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 regression profile is invalid")
|
||||
for key in ("profile_id", "pipeline_id", "experiment_id", "run_label"):
|
||||
if not isinstance(profile.get(key), str) or not str(profile[key]).strip():
|
||||
raise M48SmallStaticRegressionError("M4.8 regression profile identity is invalid")
|
||||
return encoded, profile
|
||||
|
||||
|
||||
def _read_correction(path: Path, pack_id: str) -> tuple[bytes, dict[str, Any]]:
|
||||
encoded, correction = _read_json_bytes(
|
||||
path,
|
||||
maximum=16 * 1024 * 1024,
|
||||
label="M4.8 correction snapshot",
|
||||
)
|
||||
assistance = _object(correction.get("assistance"), "M4.8 correction assistance")
|
||||
if (
|
||||
correction.get("schema_version") != _CORRECTION_SCHEMA
|
||||
or correction.get("pack_id") != pack_id
|
||||
or correction.get("state") not in {"saved", "frozen"}
|
||||
or not _integer(correction.get("revision"))
|
||||
or int(correction["revision"]) < 1
|
||||
or not isinstance(correction.get("session_id"), str)
|
||||
or not isinstance(correction.get("updated_at_utc"), str)
|
||||
or assistance.get("candidate_predictions_seen") is not True
|
||||
or assistance.get("independent_truth_eligible") is not False
|
||||
or correction.get("authority") != _AUTHORITY
|
||||
or not isinstance(correction.get("clips"), list)
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 correction snapshot is invalid")
|
||||
return encoded, correction
|
||||
|
||||
|
||||
def _assisted_anchors(correction: dict[str, Any]) -> tuple[dict[str, Any], ...]:
|
||||
anchors: list[dict[str, Any]] = []
|
||||
for clip_raw in correction["clips"]:
|
||||
clip = _object(clip_raw, "M4.8 correction clip")
|
||||
if clip.get("review_state") != "reviewed":
|
||||
continue
|
||||
clip_id = clip.get("clip_id")
|
||||
tracklets = clip.get("tracklets")
|
||||
if not isinstance(clip_id, str) or not isinstance(tracklets, list):
|
||||
raise M48SmallStaticRegressionError("M4.8 correction clip is invalid")
|
||||
for tracklet_raw in tracklets:
|
||||
tracklet = _object(tracklet_raw, "M4.8 correction tracklet")
|
||||
object_id = tracklet.get("object_id")
|
||||
if not isinstance(object_id, str) or _OBJECT_ID.fullmatch(object_id) is None:
|
||||
continue
|
||||
keyframes = tracklet.get("keyframes")
|
||||
if not isinstance(keyframes, list) or not keyframes:
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted tracklet has no keyframes")
|
||||
for keyframe_raw in keyframes:
|
||||
keyframe = _object(keyframe_raw, "M4.8 correction keyframe")
|
||||
sequence = keyframe.get("sequence")
|
||||
extent = _extent(keyframe.get("extent_xyxy"))
|
||||
if not _integer(sequence):
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted anchor sequence is invalid")
|
||||
state = _state_for_sequence(tracklet, int(sequence))
|
||||
anchor_identity = {
|
||||
"clip_id": clip_id,
|
||||
"object_id": object_id,
|
||||
"sequence": int(sequence),
|
||||
"extent_xyxy": extent,
|
||||
}
|
||||
anchors.append({
|
||||
"schema_version": M48_SMALL_STATIC_ANCHOR_SCHEMA,
|
||||
"anchor_id": "anchor-" + _canonical_sha256(anchor_identity)[:24],
|
||||
**anchor_identity,
|
||||
"visibility": keyframe.get("visibility"),
|
||||
"geometry_association": state.get("geometry_association"),
|
||||
"freshness": state.get("freshness"),
|
||||
"motion": state.get("motion"),
|
||||
"threat": state.get("threat"),
|
||||
"requires_avoidance_or_clearance": bool(
|
||||
state.get("critical_corridor_obstacle")
|
||||
),
|
||||
"authority": "operator-assisted-development-anchor-not-truth",
|
||||
})
|
||||
anchors.sort(key=lambda row: (str(row["clip_id"]), int(row["sequence"]), str(row["object_id"])))
|
||||
if len({str(row["anchor_id"]) for row in anchors}) != len(anchors):
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted anchor identity collided")
|
||||
return tuple(anchors)
|
||||
|
||||
|
||||
def _state_for_sequence(tracklet: dict[str, Any], sequence: int) -> dict[str, Any]:
|
||||
segments = tracklet.get("state_segments")
|
||||
if not isinstance(segments, list):
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted state segments are invalid")
|
||||
matches = [
|
||||
_object(row, "M4.8 assisted state segment")
|
||||
for row in segments
|
||||
if isinstance(row, dict)
|
||||
and _integer(row.get("start_sequence"))
|
||||
and _integer(row.get("end_sequence"))
|
||||
and int(row["start_sequence"]) <= sequence <= int(row["end_sequence"])
|
||||
]
|
||||
if len(matches) != 1:
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted anchor state is ambiguous")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _compare_anchor(
|
||||
anchor: dict[str, Any],
|
||||
prediction_rows: dict[tuple[str, int], dict[str, Any]],
|
||||
threshold: float,
|
||||
) -> dict[str, Any]:
|
||||
key = (str(anchor["clip_id"]), int(anchor["sequence"]))
|
||||
row = prediction_rows.get(key)
|
||||
if row is None or row.get("terminal_outcome") != "delivered":
|
||||
raise M48SmallStaticRegressionError("M4.8 assisted anchor lacks delivered prediction row")
|
||||
objects = row.get("objects")
|
||||
if not isinstance(objects, list):
|
||||
raise M48SmallStaticRegressionError("M4.8 prediction objects are invalid")
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for raw in objects:
|
||||
item = _object(raw, "M4.8 prediction object")
|
||||
normalized.append({
|
||||
"prediction_id": item.get("prediction_id"),
|
||||
"extent_xyxy": _extent(item.get("extent_xyxy")),
|
||||
"geometry_association": item.get("geometry_association"),
|
||||
"freshness": item.get("freshness"),
|
||||
"motion": item.get("motion"),
|
||||
"threat": item.get("threat"),
|
||||
})
|
||||
ranked = sorted(
|
||||
((_iou(anchor["extent_xyxy"], item["extent_xyxy"]), item) for item in normalized),
|
||||
key=lambda pair: (pair[0], str(pair[1].get("prediction_id"))),
|
||||
reverse=True,
|
||||
)
|
||||
best_iou, best = ranked[0] if ranked else (0.0, None)
|
||||
return {
|
||||
"schema_version": M48_SMALL_STATIC_COMPARISON_SCHEMA,
|
||||
"anchor_id": anchor["anchor_id"],
|
||||
"clip_id": anchor["clip_id"],
|
||||
"sequence": anchor["sequence"],
|
||||
"source_time_ns": row.get("source_time_ns"),
|
||||
"anchor_extent_xyxy": anchor["extent_xyxy"],
|
||||
"requires_avoidance_or_clearance": anchor["requires_avoidance_or_clearance"],
|
||||
"worker_candidate_count": len(normalized),
|
||||
"worker_objects": normalized,
|
||||
"best_prediction_id": best.get("prediction_id") if best else None,
|
||||
"best_iou": best_iou,
|
||||
"extent_iou_threshold": threshold,
|
||||
"matched_at_threshold": best_iou >= threshold,
|
||||
"outcome": "recalled" if best_iou >= threshold else "missed-assisted-anchor",
|
||||
}
|
||||
|
||||
|
||||
def _publish_result(
|
||||
*,
|
||||
destination: Path,
|
||||
identity: dict[str, Any],
|
||||
created_at_utc: str,
|
||||
accepted: bool,
|
||||
report: dict[str, Any],
|
||||
anchors: tuple[dict[str, Any], ...],
|
||||
comparisons: tuple[dict[str, Any], ...],
|
||||
) -> None:
|
||||
parent = destination.parent
|
||||
if parent.is_symlink():
|
||||
raise M48SmallStaticRegressionError("M4.8 regression output root must not be a symlink")
|
||||
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if not parent.is_dir():
|
||||
raise M48SmallStaticRegressionError("M4.8 regression output root is invalid")
|
||||
staging = parent / f".{destination.name}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_json(staging / "report.json", report)
|
||||
_write_jsonl(staging / "anchors.jsonl", anchors)
|
||||
_write_jsonl(staging / "comparisons.jsonl", comparisons)
|
||||
artifacts = [
|
||||
_artifact(
|
||||
staging / "anchors.jsonl",
|
||||
"assisted-regression-anchors",
|
||||
M48_SMALL_STATIC_ANCHOR_SCHEMA,
|
||||
),
|
||||
_artifact(
|
||||
staging / "comparisons.jsonl",
|
||||
"exact-frame-worker-comparisons",
|
||||
M48_SMALL_STATIC_COMPARISON_SCHEMA,
|
||||
),
|
||||
_artifact(
|
||||
staging / "report.json",
|
||||
"m48-small-static-regression-report",
|
||||
M48_SMALL_STATIC_REPORT_SCHEMA,
|
||||
),
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": M48_SMALL_STATIC_RESULT_SCHEMA,
|
||||
"result_id": destination.name,
|
||||
"identity_sha256": _canonical_sha256(identity),
|
||||
"identity": identity,
|
||||
"created_at_utc": created_at_utc,
|
||||
"accepted": accepted,
|
||||
"ground_truth": False,
|
||||
"authority": dict(_AUTHORITY),
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
_write_json(staging / "manifest.json", manifest)
|
||||
if destination.exists():
|
||||
existing = {
|
||||
path.name: _file_sha256(path)
|
||||
for path in destination.iterdir()
|
||||
if path.is_file()
|
||||
}
|
||||
proposed = {
|
||||
path.name: _file_sha256(path)
|
||||
for path in staging.iterdir()
|
||||
if path.is_file()
|
||||
}
|
||||
if existing != proposed:
|
||||
raise M48SmallStaticRegressionError("immutable M4.8 regression identity collided")
|
||||
shutil.rmtree(staging)
|
||||
return
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str, schema_version: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _file_sha256(path),
|
||||
"schema_version": schema_version,
|
||||
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _read_json_bytes(path: Path, *, maximum: int, label: str) -> tuple[bytes, dict[str, Any]]:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink() or not candidate.is_file() or candidate.stat().st_size > maximum:
|
||||
raise M48SmallStaticRegressionError(f"{label} is unavailable")
|
||||
try:
|
||||
encoded = candidate.read_bytes()
|
||||
value = json.loads(encoded)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise M48SmallStaticRegressionError(f"{label} is unreadable") from exc
|
||||
return encoded, _object(value, label)
|
||||
|
||||
|
||||
def _read_json(path: Path, *, maximum: int) -> dict[str, Any]:
|
||||
return _read_json_bytes(path, maximum=maximum, label=path.name)[1]
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > 8 * 1024 * 1024:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression rows are unavailable")
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
if line.strip():
|
||||
rows.append(_object(json.loads(line), "M4.8 regression row"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise M48SmallStaticRegressionError("M4.8 regression rows are unreadable") from exc
|
||||
return rows
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None:
|
||||
path.write_bytes(b"".join(_canonical_json(row) + b"\n" for row in rows))
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise M48SmallStaticRegressionError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _integer(value: object) -> bool:
|
||||
return isinstance(value, int) and not isinstance(value, bool)
|
||||
|
||||
|
||||
def _rate(value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and 0.0 < float(value) <= 1.0
|
||||
)
|
||||
|
||||
|
||||
def _extent(value: object) -> list[float]:
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 4
|
||||
or any(not isinstance(item, (int, float)) or isinstance(item, bool) for item in value)
|
||||
):
|
||||
raise M48SmallStaticRegressionError("M4.8 extent is invalid")
|
||||
extent = [float(item) for item in value]
|
||||
if not (0.0 <= extent[0] < extent[2] <= 1.0 and 0.0 <= extent[1] < extent[3] <= 1.0):
|
||||
raise M48SmallStaticRegressionError("M4.8 extent is outside the camera plane")
|
||||
return extent
|
||||
|
||||
|
||||
def _iou(left: list[float], right: list[float]) -> float:
|
||||
x1 = max(left[0], right[0])
|
||||
y1 = max(left[1], right[1])
|
||||
x2 = min(left[2], right[2])
|
||||
y2 = min(left[3], right[3])
|
||||
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
||||
left_area = (left[2] - left[0]) * (left[3] - left[1])
|
||||
right_area = (right[2] - right[0]) * (right[3] - right[1])
|
||||
union = left_area + right_area - intersection
|
||||
return intersection / union if union > 0.0 else 0.0
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
return hashlib.sha256(_canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _utc_timestamp(value: object) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise M48SmallStaticRegressionError("M4.8 run creation time is invalid")
|
||||
text = value.strip()
|
||||
try:
|
||||
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise M48SmallStaticRegressionError("M4.8 run creation time is invalid") from exc
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
raise M48SmallStaticRegressionError("M4.8 run creation time must be UTC")
|
||||
return parsed.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48_SMALL_STATIC_RESULT_SCHEMA",
|
||||
"M48SmallStaticRegressionError",
|
||||
"M48SmallStaticRegressionResult",
|
||||
"build_m48_small_static_passage_regression",
|
||||
"read_m48_small_static_passage_regression",
|
||||
]
|
||||
@@ -56,6 +56,7 @@ from k1link.compute.e40_perception_product_gate import (
|
||||
read_e40_perception_product_gate,
|
||||
)
|
||||
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceVariant
|
||||
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
||||
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
|
||||
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
|
||||
@@ -259,7 +260,10 @@ def _advanced_index(
|
||||
specs: tuple[_AdvancedIndexSpec, ...],
|
||||
) -> dict[str, object]:
|
||||
items: list[dict[str, object]] = []
|
||||
selected_work_ids: set[str] = set()
|
||||
for work_id, provider, pattern, document_name, schema_version in specs:
|
||||
if work_id in selected_work_ids:
|
||||
continue
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
continue
|
||||
@@ -273,6 +277,7 @@ def _advanced_index(
|
||||
schema_version=schema_version,
|
||||
)
|
||||
)
|
||||
selected_work_ids.add(work_id)
|
||||
break
|
||||
except (json.JSONDecodeError, OSError, TypeError, ValueError):
|
||||
continue
|
||||
@@ -290,17 +295,18 @@ def _registry_index_specs(
|
||||
return tuple(
|
||||
(
|
||||
definition.work_id,
|
||||
_evidence_root_provider(definition, runtime_root_provider),
|
||||
definition.result_id_pattern,
|
||||
definition.document_name,
|
||||
definition.result_schema_version,
|
||||
_evidence_root_provider(variant, runtime_root_provider),
|
||||
variant.result_id_pattern,
|
||||
variant.document_name,
|
||||
variant.result_schema_version,
|
||||
)
|
||||
for definition in registry.definitions
|
||||
for variant in reversed(definition.evidence_variants)
|
||||
)
|
||||
|
||||
|
||||
def _evidence_root_provider(
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
definition: LaboratoryEvidenceDefinition | LaboratoryEvidenceVariant,
|
||||
runtime_root_provider: RootProvider,
|
||||
) -> RootProvider:
|
||||
def result_root_provider() -> Path | None:
|
||||
|
||||
@@ -23,15 +23,23 @@ from k1link.compute import (
|
||||
RecordedPerceptionOverlayMux,
|
||||
RecordedPerceptionOverlayStore,
|
||||
)
|
||||
from k1link.compute.pipeline_telemetry import JsonlPipelineTelemetrySink
|
||||
from k1link.laboratory import (
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryEvidenceReportService,
|
||||
LaboratoryExecutionRegistry,
|
||||
LaboratoryRunner,
|
||||
LaboratoryValueReviewRegistry,
|
||||
)
|
||||
from k1link.laboratory.m48_raw_evidence import (
|
||||
M48_EXPECTED_THREAT_RESULT_ID,
|
||||
M48RawEvidenceError,
|
||||
M48RawEvidenceReader,
|
||||
)
|
||||
from k1link.sessions import (
|
||||
MaterializedRecording,
|
||||
RecordedCameraFrameService,
|
||||
RecordedCameraPlaybackSource,
|
||||
RecordedMediaInspector,
|
||||
RecordedMediaManifest,
|
||||
RecordingPreparationQueueFull,
|
||||
@@ -116,6 +124,7 @@ from k1link.web.laboratory_report_api import build_laboratory_report_router
|
||||
from k1link.web.lidar_api import build_lidar_router
|
||||
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
|
||||
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
|
||||
from k1link.web.m48_object_quality_api import build_m48_object_quality_router
|
||||
from k1link.web.map_api import (
|
||||
MapGatewayConfiguration,
|
||||
MapGatewayProxy,
|
||||
@@ -151,6 +160,13 @@ LABORATORY_EXECUTION_REGISTRY = LaboratoryExecutionRegistry.from_file(
|
||||
REPOSITORY_ROOT / "config" / "laboratory-execution.json",
|
||||
LABORATORY_EVIDENCE_REGISTRY,
|
||||
)
|
||||
LABORATORY_RUNNER = LaboratoryRunner(
|
||||
registry=LABORATORY_EXECUTION_REGISTRY,
|
||||
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
|
||||
sink=JsonlPipelineTelemetrySink(
|
||||
REPOSITORY_ROOT / ".runtime" / "telemetry" / "laboratory-runs.jsonl"
|
||||
),
|
||||
)
|
||||
LABORATORY_VALUE_REVIEW_REGISTRY = LaboratoryValueReviewRegistry.from_file(
|
||||
REPOSITORY_ROOT / "config" / "laboratory-value-review.json"
|
||||
)
|
||||
@@ -204,6 +220,20 @@ session_recorded_camera_frame_service = (
|
||||
if _ffmpeg is not None
|
||||
else None
|
||||
)
|
||||
try:
|
||||
m48_raw_evidence_reader: M48RawEvidenceReader | None = M48RawEvidenceReader.from_repository(
|
||||
repository_root=REPOSITORY_ROOT,
|
||||
threat_result_root=(
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "m4"
|
||||
/ "replay-threat"
|
||||
/ M48_EXPECTED_THREAT_RESULT_ID
|
||||
),
|
||||
)
|
||||
except (M48RawEvidenceError, OSError, ValueError):
|
||||
m48_raw_evidence_reader = None
|
||||
session_legacy_perception_overlay_store = (
|
||||
RecordedPerceptionOverlayStore(
|
||||
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
|
||||
@@ -295,6 +325,20 @@ session_recording_preparation_manager = SessionRecordingPreparationManager(
|
||||
)
|
||||
|
||||
|
||||
def _m48_recorded_camera_playback_source(
|
||||
session_id: str,
|
||||
) -> RecordedCameraPlaybackSource:
|
||||
"""Publish the durable replay package before exposing its manifest URL."""
|
||||
|
||||
if session_recorded_camera_frame_service is None:
|
||||
raise RuntimeError("recorded camera playback is unavailable")
|
||||
command = session_store.prepare_replay(session_id, speed=1.0, loop=False)
|
||||
snapshot = session_recording_preparation_manager.restore_published(command)
|
||||
if snapshot is None or snapshot.state != "ready" or snapshot.recorded_media is None:
|
||||
raise RuntimeError("recorded camera playback package is not published")
|
||||
return session_recorded_camera_frame_service.playback_source(session_id)
|
||||
|
||||
|
||||
def refresh_observation_catalog() -> tuple[str, ...]:
|
||||
"""Discover completed or recoverable local evidence without copying payloads."""
|
||||
|
||||
@@ -859,6 +903,47 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m48_object_quality_router(
|
||||
pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-quality-packs"
|
||||
),
|
||||
workflow_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "m48-object-quality"
|
||||
),
|
||||
truth_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-truth-seals"
|
||||
),
|
||||
result_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-quality-results"
|
||||
),
|
||||
small_static_result_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "m48"
|
||||
/ "small-static-passage-regression-results"
|
||||
),
|
||||
camera_frame_provider=(
|
||||
session_recorded_camera_frame_service.extract
|
||||
if session_recorded_camera_frame_service is not None
|
||||
else None
|
||||
),
|
||||
camera_playback_provider=(
|
||||
_m48_recorded_camera_playback_source
|
||||
if session_recorded_camera_frame_service is not None
|
||||
else None
|
||||
),
|
||||
spatial_evidence_provider=m48_raw_evidence_reader,
|
||||
evaluation_runner=LABORATORY_RUNNER,
|
||||
evaluation_receipt_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "laboratory-run-receipts"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_e47_semantic_slam_router(
|
||||
root_provider=lambda: (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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