diff --git a/config/laboratories/m48s-fixed-class-detector.json b/config/laboratories/m48s-fixed-class-detector.json new file mode 100644 index 0000000..6e11b6d --- /dev/null +++ b/config/laboratories/m48s-fixed-class-detector.json @@ -0,0 +1,10 @@ +{ + "schema_version": "missioncore.laboratory-evidence-definition/v1", + "work_id": "m48s-fixed-class-detector", + "evidence": { + "runtime_relative_root": "m48s-semantic-shadow/fixed-class-detector-lab-results", + "result_id_prefix": "m48s-fixed-class-detector-lab", + "document_name": "manifest.json", + "schema_version": "missioncore.m48s-fixed-class-detector-lab/v1" + } +} diff --git a/config/laboratory-execution.json b/config/laboratory-execution.json index 05feb9e..542113e 100644 --- a/config/laboratory-execution.json +++ b/config/laboratory-execution.json @@ -114,6 +114,20 @@ "run": "missioncore.laboratory-run/v1", "evidence": "missioncore.e47-semantic-slam-result/v1" } + }, + { + "work_id": "m48s-fixed-class-detector", + "lifecycle": "experimental", + "isolation": "bounded-adapter", + "adapter_id": "experimental.m48s-fixed-class-detector/v1", + "input_roles": ["repository_root"], + "contracts": { + "source": "missioncore.m48s-sealed-detector-evidence/v1", + "provider": "missioncore.rf-detr-risk-shadow-provider/v1", + "graph": "missioncore.m48s-fixed-class-detector-lab-graph/v1", + "run": "missioncore.laboratory-run/v1", + "evidence": "missioncore.m48s-fixed-class-detector-lab/v1" + } } ], "legacy_work_ids": [ diff --git a/config/laboratory-value-review.json b/config/laboratory-value-review.json index 52db889..60d1664 100644 --- a/config/laboratory-value-review.json +++ b/config/laboratory-value-review.json @@ -1,6 +1,6 @@ { "schema_version": "missioncore.laboratory-value-review-registry/v1", - "reviewed_at_utc": "2026-08-05T15:34:00Z", + "reviewed_at_utc": "2026-08-25T11:06:28Z", "entries": [ { "catalog_id": "e28-local-surface", @@ -246,6 +246,13 @@ "signal": "retained", "lifecycle": "current", "visual_evidence": "available" + }, + { + "catalog_id": "m48s-fixed-class-detector", + "evidence_id": "m48s-fixed-class-detector-lab-d1bac05a9e43d407b0f931105cc0e84183ef9ff37666911f03c41486beeb7ef9", + "signal": "progress", + "lifecycle": "current", + "visual_evidence": "available" } ] } diff --git a/experiments/perception/seal_m48s_fixed_class_detector_lab.py b/experiments/perception/seal_m48s_fixed_class_detector_lab.py new file mode 100644 index 0000000..400c094 --- /dev/null +++ b/experiments/perception/seal_m48s_fixed_class_detector_lab.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Publish the sealed M4.8S detector evidence as an immutable LAB result.""" + +from __future__ import annotations + +from pathlib import Path + +from k1link.laboratory.m48s_fixed_class_detector_lab import ( + build_m48s_fixed_class_detector_lab, +) + + +def main() -> int: + repository = Path(__file__).resolve().parents[2] + result = build_m48s_fixed_class_detector_lab( + repository_root=repository, + output_root=( + repository + / ".runtime/compute-experiments/m48s-semantic-shadow/" + "fixed-class-detector-lab-results" + ), + ) + print(result.result_id) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/k1link/laboratory/execution.py b/src/k1link/laboratory/execution.py index 0b5301d..ca4eeb5 100644 --- a/src/k1link/laboratory/execution.py +++ b/src/k1link/laboratory/execution.py @@ -321,9 +321,27 @@ def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]: "canonical.e35-degradation-recovery/v1": _run_e35, "canonical.e46j-raw-fisheye-realtime/v1": _run_e46j, "experimental.e47-semantic-slam-shadow/v1": _run_e47, + "experimental.m48s-fixed-class-detector/v1": _run_m48s_fixed_class_detector, } +def _run_m48s_fixed_class_detector( + request: LaboratoryRunRequest, +) -> LaboratoryAdapterResult: + from k1link.laboratory.m48s_fixed_class_detector_lab import ( + build_m48s_fixed_class_detector_lab, + ) + + result = build_m48s_fixed_class_detector_lab( + repository_root=request.inputs["repository_root"], + output_root=request.output_root, + ) + return LaboratoryAdapterResult( + result_root=result.result_root, + result_id=result.result_id, + ) + + def _run_m48_small_static_passage_regression( request: LaboratoryRunRequest, ) -> LaboratoryAdapterResult: diff --git a/src/k1link/laboratory/m48s_fixed_class_detector_lab.py b/src/k1link/laboratory/m48s_fixed_class_detector_lab.py new file mode 100644 index 0000000..d93c35d --- /dev/null +++ b/src/k1link/laboratory/m48s_fixed_class_detector_lab.py @@ -0,0 +1,892 @@ +"""Build the immutable M4.8S fixed-class detector laboratory projection.""" + +from __future__ import annotations + +import hashlib +import json +import shutil +import tempfile +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final, cast + +from k1link.perception.fixed_class_detector_tournament import ( + canonical_json, + false_authority, + sha256_path, +) + +LAB_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-lab/v1" +CATALOG_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-frame-catalog/v1" +FRAME_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-frame/v1" +REPORT_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-report/v1" +METHOD_SCHEMA: Final = "missioncore.laboratory-method/v1" +RESULT_PREFIX: Final = "m48s-fixed-class-detector-lab-" +TOURNAMENT_ID: Final = ( + "m48s-fixed-detector-tournament-" + "0e61d75e6dc575d53e4bb98772a41d240fe627ad642de5178beb1154636e1299" +) +DEPLOYMENT_ID: Final = ( + "m48s-rf-detr-deployment-gate-2feb9e1b12a5588951ad35d63bf23cf6bdd579d54b5329d46d7696f88c444547" +) +REFERENCE_GRAPH_ID: Final = ( + "m48s-reference-graph-shadow-gate-" + "e8da7a521768daba0ead1a6e4803871ce3a85f91a7d8ee36c5719ac10433e791" +) +REFERENCE_GRAPH_REPLAY_ID: Final = ( + "m48s-reference-graph-replay-" + "16d69d610c22e6f42071b8378cd75dfa6b95db4ceb800f9c7508fa3673504478" +) +INTEGRATED_STATUS: Final = "complete-reference-graph-shadow-passed-production-not-authorized" +YOLOX_ID: Final = ( + "m48s-yolox-all-coco-shadow-7dbe6043b3fc12c7ddb162f609f883d86b34a4f2dd3785a632795f257e192d06" +) +SOURCE_SHA256: Final = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8" +ENGINE_SHA256: Final = "986399ce706b7380472cf5e473232249fed6e628971d8007f6609e83128d46b8" +RISK_LABELS: Final = frozenset( + { + "person", + "bicycle", + "car", + "motorcycle", + "bus", + "truck", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "skateboard", + } +) +FRAME_IDS: Final = ( + "000121", + "000131", + "000253", + "000275", + "000443", + "000463", + "001094", + "001228", + "001454", + "001856", + "002386", +) + + +class M48SFixedClassDetectorLabError(RuntimeError): + """The sealed detector evidence cannot produce an honest LAB result.""" + + +@dataclass(frozen=True, slots=True) +class M48SFixedClassDetectorLabResult: + result_root: Path + result_id: str + manifest: dict[str, Any] + + +def build_m48s_fixed_class_detector_lab( + *, + repository_root: Path, + output_root: Path, +) -> M48SFixedClassDetectorLabResult: + repository = repository_root.expanduser().resolve(strict=True) + if not repository.is_dir(): + raise M48SFixedClassDetectorLabError("repository root is invalid") + runtime = repository / ".runtime/compute-experiments/m48s-semantic-shadow" + tournament_path = ( + runtime / "fixed-detector-tournament-results" / TOURNAMENT_ID / "manifest.json" + ) + deployment_root = runtime / "rf-detr-deployment-results" / DEPLOYMENT_ID + deployment_path = deployment_root / "manifest.json" + load_path = deployment_root / "load_result.json" + reference_graph_root = runtime / "reference-graph-shadow-results" / REFERENCE_GRAPH_ID + reference_graph_path = reference_graph_root / "manifest.json" + reference_graph_worker_path = reference_graph_root / "worker-result.json" + reference_graph_replay_root = ( + runtime / "reference-graph-replay-results" / REFERENCE_GRAPH_REPLAY_ID + ) + reference_graph_replay_path = reference_graph_replay_root / "manifest.json" + reference_graph_replay_worker_path = reference_graph_replay_root / "worker-result.json" + reference_graph_replay_frames_path = reference_graph_replay_root / "frames.jsonl" + yolox_root = runtime / "yolox-all-coco-results" / YOLOX_ID + yolox_manifest_path = yolox_root / "manifest.json" + yolox_frames_path = yolox_root / "frames.jsonl" + candidate_root = runtime / "fixed-detector-tournament-worker" + dfine_path = candidate_root / "dfine-s-worker.json" + rf_detr_path = candidate_root / "rf-detr-large-triton-worker.json" + source_root = runtime / "raw-11-frames-v1" + profile_path = repository / "config/perception/rf-detr-large-risk-shadow-v0.json" + for path in ( + tournament_path, + deployment_path, + load_path, + reference_graph_path, + reference_graph_worker_path, + reference_graph_replay_path, + reference_graph_replay_worker_path, + reference_graph_replay_frames_path, + yolox_manifest_path, + yolox_frames_path, + dfine_path, + rf_detr_path, + profile_path, + ): + if path.is_symlink() or not path.is_file(): + raise M48SFixedClassDetectorLabError( + f"required sealed evidence is missing: {path.name}" + ) + + tournament = _read_object(tournament_path) + deployment = _read_object(deployment_path) + load = _read_object(load_path) + reference_graph = _read_object(reference_graph_path) + reference_graph_worker = _read_object(reference_graph_worker_path) + reference_graph_replay = _read_object(reference_graph_replay_path) + reference_graph_replay_worker = _read_object(reference_graph_replay_worker_path) + yolox_manifest = _read_object(yolox_manifest_path) + dfine = _read_object(dfine_path) + rf_detr = _read_object(rf_detr_path) + profile = _read_object(profile_path) + yolox_frames = _read_jsonl(yolox_frames_path) + _validate_inputs( + tournament=tournament, + deployment=deployment, + load=load, + reference_graph=reference_graph, + reference_graph_worker=reference_graph_worker, + reference_graph_replay=reference_graph_replay, + reference_graph_replay_worker=reference_graph_replay_worker, + reference_graph_replay_frames_path=reference_graph_replay_frames_path, + yolox_manifest=yolox_manifest, + dfine=dfine, + rf_detr=rf_detr, + profile=profile, + yolox_frames=yolox_frames, + ) + + source_paths = {frame_id: source_root / f"frame-{frame_id}.jpg" for frame_id in FRAME_IDS} + if any(path.is_symlink() or not path.is_file() for path in source_paths.values()): + raise M48SFixedClassDetectorLabError("the exact 11-frame visual slice is incomplete") + source_descriptors = [ + { + "frame_id": frame_id, + "source_sequence": int(frame_id), + "sha256": sha256_path(source_paths[frame_id]), + "byte_length": source_paths[frame_id].stat().st_size, + } + for frame_id in FRAME_IDS + ] + method = _method( + profile=profile, + tournament=tournament, + deployment=deployment, + reference_graph=reference_graph, + ) + identity = { + "schema_version": LAB_SCHEMA, + "source": { + "source_session_id": "RAVNOVES00", + "recording_sha256": SOURCE_SHA256, + "camera_source_id": "sensor.camera.right", + "camera_raster": [800, 600], + "evidence_frame_count": len(FRAME_IDS), + "replay_frame_count": 4489, + "frames": source_descriptors, + }, + "configuration": { + "comparison_threshold": 0.5, + "display_modes": ["source", "yolox", "dfine", "rf-detr"], + "replay_display_modes": ["video", "camera", "3d", "plan"], + "camera_point_overlay": "factory-kb4-exact", + "world_state_delivery": "source-paced-latest-wins", + "single_inference_per_frame": True, + "risk_labels": sorted(RISK_LABELS), + "geometry_owns_static_occupancy": True, + "unknown_stationary_response": "route-around", + "unknown_moving_response": "conservative-risk", + }, + "inputs": { + "tournament_result_id": TOURNAMENT_ID, + "tournament_document_sha256": sha256_path(tournament_path), + "deployment_result_id": DEPLOYMENT_ID, + "deployment_document_sha256": sha256_path(deployment_path), + "reference_graph_result_id": REFERENCE_GRAPH_ID, + "reference_graph_document_sha256": sha256_path(reference_graph_path), + "reference_graph_worker_sha256": sha256_path(reference_graph_worker_path), + "reference_graph_replay_result_id": REFERENCE_GRAPH_REPLAY_ID, + "reference_graph_replay_document_sha256": sha256_path( + reference_graph_replay_path + ), + "reference_graph_replay_worker_sha256": sha256_path( + reference_graph_replay_worker_path + ), + "reference_graph_replay_frames_sha256": sha256_path( + reference_graph_replay_frames_path + ), + "yolox_result_id": YOLOX_ID, + "yolox_document_sha256": sha256_path(yolox_manifest_path), + }, + "method": method, + "authority": false_authority(), + } + identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest() + result_id = RESULT_PREFIX + identity_sha256 + completed_utc_ns = reference_graph_replay_worker.get("completed_utc_ns") + if not isinstance(completed_utc_ns, int) or isinstance(completed_utc_ns, bool): + raise M48SFixedClassDetectorLabError( + "complete reference-graph completion time is unavailable" + ) + created_at_utc = ( + datetime.fromtimestamp(completed_utc_ns / 1_000_000_000, UTC) + .isoformat(timespec="microseconds") + .replace("+00:00", "Z") + ) + candidates = _candidate_summaries( + tournament=tournament, + yolox_manifest=yolox_manifest, + dfine=dfine, + rf_detr=rf_detr, + deployment=deployment, + ) + metrics = _metrics( + deployment=deployment, + load=load, + reference_graph=reference_graph, + candidates=candidates, + ) + decision = { + "bounded_question_accepted": True, + "selected_candidate": "rf-detr", + "ready_for_reference_graph_shadow": True, + "integrated_world_state_gate_evaluated": True, + "integrated_world_state_gate_passed": True, + "full_replay_visual_published": True, + "detector_replacement_authorized": False, + "production_accepted": False, + } + limitations = [ + "The 11-frame slice is diagnostic and has no independent semantic ground truth.", + ( + "The complete reference-graph replay qualifies runtime behavior, but has no " + "independent track-identity or risk-policy truth." + ), + "COCO has no dedicated scooter class; unknown moving objects remain conservative hazards.", + ( + "Camera boxes do not replace geometry-owned occupancy or grant navigation/safety " + "authority." + ), + ( + "Eight source frames were superseded by the qualified latest-wins graph; their " + "camera/LiDAR source evidence remains visible without invented world state." + ), + ] + + root = output_root.expanduser().absolute() + root.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = root / result_id + if destination.exists(): + raise M48SFixedClassDetectorLabError("immutable M4.8S LAB result already exists") + temporary = Path(tempfile.mkdtemp(prefix=".m48s-fixed-class-lab-", dir=root)) + try: + (temporary / "frames").mkdir(mode=0o700) + yolox_by_frame = _yolox_by_frame(yolox_frames) + dfine_by_frame = _worker_by_frame(dfine) + rf_detr_by_frame = _worker_by_frame(rf_detr) + frame_descriptors: list[dict[str, object]] = [] + for frame_id in FRAME_IDS: + camera_name = f"frames/frame-{frame_id}.jpg" + detail_name = f"frame-{frame_id}.json" + camera_path = temporary / camera_name + shutil.copyfile(source_paths[frame_id], camera_path) + detections_by_model: dict[str, list[dict[str, object]]] = { + "yolox": _qualified(yolox_by_frame[frame_id]), + "dfine": _qualified(dfine_by_frame[frame_id]), + "rf-detr": _qualified(rf_detr_by_frame[frame_id]), + } + frame_document = { + "schema_version": FRAME_SCHEMA, + "result_id": result_id, + "frame_id": frame_id, + "source_sequence": int(frame_id), + "camera": { + "path": camera_name, + "media_type": "image/jpeg", + "width": 800, + "height": 600, + "sha256": sha256_path(camera_path), + "exact_source_frame": True, + }, + "comparison_threshold": 0.5, + "detections": detections_by_model, + "ground_truth_available": False, + "authority": false_authority(), + } + detail_path = temporary / detail_name + detail_path.write_bytes(canonical_json(frame_document) + b"\n") + frame_descriptors.append( + { + "frame_id": frame_id, + "source_sequence": int(frame_id), + "camera_path": camera_name, + "camera_sha256": sha256_path(camera_path), + "camera_byte_length": camera_path.stat().st_size, + "detail_path": detail_name, + "detail_sha256": sha256_path(detail_path), + "detail_byte_length": detail_path.stat().st_size, + "counts": {key: len(value) for key, value in detections_by_model.items()}, + } + ) + catalog = { + "schema_version": CATALOG_SCHEMA, + "result_id": result_id, + "frame_count": len(frame_descriptors), + "frames": frame_descriptors, + } + catalog_path = temporary / "catalog.json" + catalog_path.write_bytes(canonical_json(catalog) + b"\n") + shutil.copyfile(tournament_path, temporary / "tournament.json") + shutil.copyfile(deployment_path, temporary / "deployment.json") + shutil.copyfile(reference_graph_path, temporary / "reference-graph.json") + shutil.copyfile( + reference_graph_worker_path, + temporary / "reference-graph-worker-result.json", + ) + shutil.copyfile( + reference_graph_replay_path, + temporary / "reference-graph-replay.json", + ) + shutil.copyfile( + reference_graph_replay_worker_path, + temporary / "reference-graph-replay-worker-result.json", + ) + shutil.copyfile( + reference_graph_replay_frames_path, + temporary / "reference-graph-replay-frames.jsonl", + ) + report = { + "schema_version": REPORT_SCHEMA, + "result_id": result_id, + "source": identity["source"], + "configuration": identity["configuration"], + "method": method, + "execution": { + "detector_load": load["execution"], + "complete_reference_graph": reference_graph["identity"]["evidence"]["execution"], + }, + "metrics": metrics, + "acceptance": { + "detector_load": load["checks"], + "complete_reference_graph": reference_graph["identity"]["evidence"]["checks"], + }, + "decision": decision, + "limitations": limitations, + "authority": false_authority(), + "visual_evidence": { + "kind": "full-reference-graph-recorded-replay", + "frame_count": 4489, + "world_state_frame_count": 4481, + "superseded_frame_count": 8, + "modes": ["video", "camera", "3d", "plan"], + "camera_layers": ["rf-detr", "points"], + "ground_truth": False, + }, + } + report_path = temporary / "report.json" + report_path.write_bytes(canonical_json(report) + b"\n") + artifacts = _artifact_manifest(temporary) + manifest = { + "schema_version": LAB_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "status": INTEGRATED_STATUS, + "completed": True, + "bounded_question_accepted": True, + "ground_truth": False, + "catalog": { + "path": "catalog.json", + "sha256": sha256_path(catalog_path), + "byte_length": catalog_path.stat().st_size, + }, + "method": method, + "metrics": metrics, + "decision": decision, + "limitations": limitations, + "authority": false_authority(), + "artifacts": artifacts, + } + (temporary / "manifest.json").write_bytes(canonical_json(manifest) + b"\n") + temporary.replace(destination) + except BaseException: + shutil.rmtree(temporary, ignore_errors=True) + raise + return M48SFixedClassDetectorLabResult( + result_root=destination, + result_id=result_id, + manifest=manifest, + ) + + +def _validate_inputs( + *, + tournament: dict[str, Any], + deployment: dict[str, Any], + load: dict[str, Any], + reference_graph: dict[str, Any], + reference_graph_worker: dict[str, Any], + reference_graph_replay: dict[str, Any], + reference_graph_replay_worker: dict[str, Any], + reference_graph_replay_frames_path: Path, + yolox_manifest: dict[str, Any], + dfine: dict[str, Any], + rf_detr: dict[str, Any], + profile: dict[str, Any], + yolox_frames: list[dict[str, Any]], +) -> None: + decision = deployment.get("decision") + graph_identity = reference_graph.get("identity") + graph_artifacts = reference_graph.get("artifacts") + graph_decision = graph_identity.get("decision") if isinstance(graph_identity, dict) else None + graph_checks = reference_graph_worker.get("checks") + replay_identity = reference_graph_replay.get("identity") + replay_artifacts = reference_graph_replay.get("artifacts") + replay_frame_summary = ( + replay_identity.get("evidence", {}).get("frame_summary") + if isinstance(replay_identity, dict) + and isinstance(replay_identity.get("evidence"), dict) + else None + ) + if ( + tournament.get("result_id") != TOURNAMENT_ID + or tournament.get("accepted") is not False + or deployment.get("result_id") != DEPLOYMENT_ID + or deployment.get("accepted") is not False + or not isinstance(decision, dict) + or decision.get("ready_for_reference_graph_shadow") is not True + or decision.get("production_accepted") is not False + or load.get("detector_load_gate_passed") is not True + or load.get("candidate_accepted") is not False + or reference_graph.get("result_id") != REFERENCE_GRAPH_ID + or reference_graph.get("schema_version") + != "missioncore.m48s-reference-graph-shadow-gate/v0" + or not isinstance(graph_identity, dict) + or graph_identity.get("graph_id") != "reference-perception-graph/v2" + or graph_identity.get("accepted") is not True + or graph_identity.get("production_accepted") is not False + or graph_identity.get("authority") != false_authority() + or not isinstance(graph_decision, dict) + or graph_decision.get("complete_reference_graph_shadow_passed") is not True + or graph_decision.get("source_paced_runtime_gate_accepted") is not True + or graph_decision.get("detector_replacement_authorized") is not False + or graph_decision.get("production_accepted") is not False + or not isinstance(graph_artifacts, dict) + or graph_artifacts.get("worker-result.json") + != hashlib.sha256(canonical_json(reference_graph_worker) + b"\n").hexdigest() + or reference_graph_worker.get("completed") is not True + or reference_graph_worker.get("integrated_runtime_gate_passed") is not True + or reference_graph_worker.get("production_accepted") is not False + or reference_graph_worker.get("authority") != false_authority() + or not isinstance(graph_checks, dict) + or not graph_checks + or not all(value is True for value in graph_checks.values()) + or reference_graph_replay.get("result_id") != REFERENCE_GRAPH_REPLAY_ID + or reference_graph_replay.get("schema_version") + != "missioncore.m48s-reference-graph-replay/v0" + or not isinstance(replay_identity, dict) + or replay_identity.get("accepted") is not True + or replay_identity.get("production_accepted") is not False + or replay_identity.get("authority") != false_authority() + or not isinstance(replay_frame_summary, dict) + or replay_frame_summary.get("source_frame_count") != 4489 + or replay_frame_summary.get("world_state_frame_count") != 4481 + or replay_frame_summary.get("superseded_frame_count") != 8 + or not isinstance(replay_artifacts, dict) + or not isinstance(replay_artifacts.get("frames.jsonl"), dict) + or replay_artifacts["frames.jsonl"].get("sha256") + != sha256_path(reference_graph_replay_frames_path) + or not isinstance(replay_artifacts.get("worker-result.json"), dict) + or replay_artifacts["worker-result.json"].get("sha256") + != hashlib.sha256(canonical_json(reference_graph_replay_worker) + b"\n").hexdigest() + or reference_graph_replay_worker.get("integrated_runtime_gate_passed") is not True + or reference_graph_replay_worker.get("production_accepted") is not False + or reference_graph_replay_worker.get("authority") != false_authority() + or yolox_manifest.get("result_id") != YOLOX_ID + or dfine.get("profile_id") != "dfine-s-coco-640-fp16/v0" + or rf_detr.get("profile_id") != "rf-detr-large-coco-704-trt11-fp16/v0" + or rf_detr.get("engine_sha256") != ENGINE_SHA256 + or profile.get("profile_id") != "rf-detr-large-coco-704-trt11-fp16-risk-shadow/v0" + or len(yolox_frames) != len(FRAME_IDS) + ): + raise M48SFixedClassDetectorLabError("sealed M4.8S evidence identity changed") + for document in ( + tournament, + deployment, + load, + reference_graph_worker, + reference_graph_replay_worker, + dfine, + rf_detr, + profile, + ): + authority = document.get("authority") + if authority is not None and authority != false_authority(): + raise M48SFixedClassDetectorLabError("sealed M4.8S evidence gained authority") + + +def _method( + *, + profile: dict[str, Any], + tournament: dict[str, Any], + deployment: dict[str, Any], + reference_graph: dict[str, Any], +) -> dict[str, object]: + candidates = tournament["candidates"] + if not isinstance(candidates, dict): + raise M48SFixedClassDetectorLabError("tournament candidates are unavailable") + dfine = candidates["dfine-s-coco-640-fp16/v0"] + rf_detr = candidates["rf-detr-large-coco-704-fp16/v0"] + if not isinstance(dfine, dict) or not isinstance(rf_detr, dict): + raise M48SFixedClassDetectorLabError("tournament candidates are invalid") + return { + "schema_version": METHOD_SCHEMA, + "completeness": "complete", + "execution_class": "ai-inference", + "pipeline_id": "raw-kb4-rf-detr-reference-graph-shadow/v1", + "components": [ + { + "kind": "source", + "name": "RAVNOVES00 RIGHT", + "version": "immutable recorded source", + "role": "11-frame visual slice and 30-minute source-paced replay", + "identity_sha256": SOURCE_SHA256, + }, + { + "kind": "model", + "name": "YOLOX-S COCO-80", + "version": "triton-yolox-s-raw-kb4-all-coco/v2", + "role": "regression baseline", + "identity_sha256": ( + "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063" + ), + }, + { + "kind": "model", + "name": "D-FINE-S COCO", + "version": str(dfine["upstream_revision"]), + "role": "fixed-class tournament candidate", + "identity_sha256": str(dfine["checkpoint_sha256"]), + }, + { + "kind": "model", + "name": "RF-DETR-L COCO", + "version": str(rf_detr["upstream_revision"]), + "role": "selected fixed-class risk detector", + "identity_sha256": str(rf_detr["checkpoint_sha256"]), + }, + { + "kind": "runtime", + "name": "TensorRT 11 + isolated Triton", + "version": "worker-006 RTX 4090 strongly-typed-fp16", + "role": "numeric parity and source-paced load qualification", + "identity_sha256": str(deployment["evidence"]["engine_sha256"]), + }, + { + "kind": "algorithm", + "name": "risk-only fixed-class qualification", + "version": str(profile["profile_id"]), + "role": "emit behavior-relevant semantics while geometry owns static occupancy", + "identity_sha256": hashlib.sha256(canonical_json(profile)).hexdigest(), + }, + { + "kind": "algorithm", + "name": "reference perception graph", + "version": str(reference_graph["identity"]["graph_id"]), + "role": ( + "geometry, temporal identity, motion, rolling occupancy, threat, " + "and class advisory" + ), + "identity_sha256": str( + reference_graph["identity"]["evidence"]["files"]["graph_config"]["sha256"] + ), + }, + ], + } + + +def _candidate_summaries( + *, + tournament: dict[str, Any], + yolox_manifest: dict[str, Any], + dfine: dict[str, Any], + rf_detr: dict[str, Any], + deployment: dict[str, Any], +) -> list[dict[str, object]]: + baseline = tournament["baseline"] + candidates = tournament["candidates"] + triton_benchmark = deployment["evidence"]["triton_benchmark"] + if not isinstance(baseline, dict) or not isinstance(candidates, dict): + raise M48SFixedClassDetectorLabError("tournament summaries are invalid") + return [ + { + "id": "yolox", + "label": "YOLOX-S", + "provider_id": baseline["provider_id"], + "capacity_fps": yolox_manifest["metrics"]["all_coco_core_capacity_fps"], + "p95_ms": yolox_manifest["metrics"]["timing_ms"]["all_coco_core_ms"]["p95"], + "frame_253_dog_detected": False, + "frame_253_dog_score": None, + "selected": False, + }, + { + "id": "dfine", + "label": "D-FINE-S", + "provider_id": dfine["provider_id"], + "capacity_fps": dfine["metrics"]["benchmark"]["core_capacity_fps"], + "p95_ms": dfine["metrics"]["benchmark"]["timing_ms"]["p95"], + "frame_253_dog_detected": False, + "frame_253_dog_score": None, + "selected": False, + }, + { + "id": "rf-detr", + "label": "RF-DETR-L", + "provider_id": rf_detr["provider_id"], + "capacity_fps": triton_benchmark["end_to_end_capacity_fps"], + "p95_ms": triton_benchmark["timing_ms"]["p95"], + "frame_253_dog_detected": True, + "frame_253_dog_score": deployment["evidence"]["tensorrt_parity"]["tensorrt_score"], + "selected": True, + }, + ] + + +def _metrics( + *, + deployment: dict[str, Any], + load: dict[str, Any], + reference_graph: dict[str, Any], + candidates: list[dict[str, object]], +) -> dict[str, object]: + graph_evidence = reference_graph["identity"]["evidence"] + graph_execution = graph_evidence["execution"] + graph_completion = graph_evidence["world_state_completion_age_ms"] + map_age = graph_evidence["local_obstacle_map_output_age_ms"] + gpu = graph_evidence["gpu"] + identity = graph_evidence["identity_continuity"] + advisory = graph_evidence["semantic_advisory"] + terminal_outcomes = graph_execution["terminal_outcomes"] + return { + "candidate_count": len(candidates), + "evidence_frame_count": len(FRAME_IDS), + "selected_candidate": "rf-detr", + "candidates": candidates, + "tensorrt_parity": deployment["evidence"]["tensorrt_parity"], + "detector_load": { + "duration_seconds": load["execution"]["wall_seconds"], + "source_frames_consumed": load["execution"]["source_frames_consumed"], + "source_frame_replacements": load["execution"]["source_frame_replacements"], + "effective_consumed_fps": load["execution"]["effective_consumed_fps"], + "end_to_end_p95_ms": load["metrics"]["end_to_end_ms"]["p95"], + "completion_age_p95_ms": load["metrics"]["detector_completion_age_ms"]["p95"], + "gpu_utilization_mean_percent": load["metrics"]["gpu"]["gpu_utilization_percent"][ + "mean" + ], + "gpu_utilization_maximum_percent": load["metrics"]["gpu"]["gpu_utilization_percent"][ + "maximum" + ], + "gpu_memory_maximum_mib": load["metrics"]["gpu"]["gpu_memory_used_mib"]["maximum"], + "queue_maximum_depth": load["execution"]["queue_maximum_depth"], + "queue_capacity": load["execution"]["queue_capacity"], + "failures": load["execution"]["failures"], + }, + "integrated_world_state": { + "duration_seconds": graph_execution["source_processing_wall_seconds"], + "source_frames_admitted": graph_execution["admitted_frames"], + "delivered_world_states": graph_execution["delivered_world_states"], + "superseded_frames": terminal_outcomes["superseded"], + "effective_world_state_fps": graph_execution["effective_world_state_fps"], + "world_state_completion_age_p95_ms": graph_completion["p95"], + "world_state_completion_age_p99_ms": graph_completion["p99"], + "world_state_completion_age_maximum_ms": graph_completion["maximum"], + "local_obstacle_map_output_age_p95_ms": map_age["p95"], + "queue_high_watermarks": graph_execution["queue_high_watermarks"], + "queue_capacity": 2, + "gpu_utilization_mean_percent": gpu["gpu_utilization_percent"]["mean"], + "gpu_utilization_maximum_percent": gpu["gpu_utilization_percent"]["maximum"], + "gpu_memory_maximum_mib": gpu["gpu_memory_used_mib"]["maximum"], + "gpu_power_maximum_w": gpu["gpu_power_w"]["maximum"], + "gpu_temperature_maximum_c": gpu["gpu_temperature_c"]["maximum"], + "unique_component_count": identity["unique_component_count"], + "multi_frame_component_count": identity["multi_frame_component_count"], + "maximum_component_publications": identity["maximum_component_publications"], + "duplicate_component_ids_within_frame": identity[ + "duplicate_component_ids_within_frame" + ], + "advisory_family_counts": advisory["advisory_family_counts"], + "semantic_hint_counts": advisory["semantic_hint_counts"], + "motion_counts": advisory["motion_counts"], + "additional_inference_passes": advisory["additional_inference_passes"], + "failures": sum( + terminal_outcomes.get(key, 0) + for key in ("failed", "stale", "rejected", "unavailable") + ), + }, + } + + +def _qualified(detections: list[dict[str, Any]]) -> list[dict[str, object]]: + result: list[dict[str, object]] = [] + for detection in detections: + label = detection.get("label") + score = detection.get("score") + bbox = detection.get("bbox_xyxy") + if ( + label not in RISK_LABELS + or not isinstance(score, (int, float)) + or isinstance(score, bool) + or float(score) < 0.5 + or not isinstance(bbox, list) + or len(bbox) != 4 + or not all(isinstance(value, (int, float)) for value in bbox) + ): + continue + result.append( + { + "label": label, + "score": round(float(score), 6), + "bbox_xyxy": [round(float(value), 3) for value in bbox], + } + ) + result.sort(key=lambda item: (-cast(float, item["score"]), str(item["label"]))) + return result + + +def _yolox_by_frame(rows: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: + result: dict[str, list[dict[str, Any]]] = {} + for row in rows: + frame_id = _frame_id(row.get("frame_name")) + detections = row.get("all_coco_detections") + if frame_id is None or not isinstance(detections, list): + raise M48SFixedClassDetectorLabError("YOLOX frame evidence is invalid") + result[frame_id] = _objects(detections, "YOLOX detections") + if tuple(sorted(result)) != tuple(sorted(FRAME_IDS)): + raise M48SFixedClassDetectorLabError("YOLOX frame slice changed") + return result + + +def _worker_by_frame(document: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: + frames = document.get("frames") + if not isinstance(frames, list): + raise M48SFixedClassDetectorLabError("candidate frame evidence is invalid") + result: dict[str, list[dict[str, Any]]] = {} + for row in _objects(frames, "candidate frames"): + frame_id = _frame_id(row.get("frame_name")) + detections = row.get("detections") + if frame_id is None or not isinstance(detections, list): + raise M48SFixedClassDetectorLabError("candidate frame evidence is invalid") + result[frame_id] = _objects(detections, "candidate detections") + if tuple(sorted(result)) != tuple(sorted(FRAME_IDS)): + raise M48SFixedClassDetectorLabError("candidate frame slice changed") + return result + + +def _frame_id(value: object) -> str | None: + if not isinstance(value, str) or not value.startswith("frame-"): + return None + stem = Path(value).stem + frame_id = stem.removeprefix("frame-") + return frame_id if frame_id in FRAME_IDS else None + + +def _artifact_manifest(root: Path) -> list[dict[str, object]]: + artifacts: list[dict[str, object]] = [] + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix() + if relative == "manifest.json": + continue + media_type = "application/json" + schema_version: str | None = None + role = "supporting-evidence" + if relative.endswith(".jpg"): + media_type = "image/jpeg" + role = "visual-evidence-camera" + elif relative.startswith("frame-"): + schema_version = FRAME_SCHEMA + role = "visual-evidence-frame" + elif relative == "catalog.json": + schema_version = CATALOG_SCHEMA + role = "visual-evidence-catalog" + elif relative == "report.json": + schema_version = REPORT_SCHEMA + role = "laboratory-report" + elif relative == "tournament.json": + role = "upstream-tournament-evidence" + elif relative == "deployment.json": + role = "upstream-deployment-evidence" + elif relative == "reference-graph.json": + role = "upstream-reference-graph-evidence" + elif relative == "reference-graph-worker-result.json": + role = "upstream-reference-graph-worker-evidence" + elif relative == "reference-graph-replay.json": + role = "upstream-full-replay-evidence" + elif relative == "reference-graph-replay-worker-result.json": + role = "upstream-full-replay-worker-evidence" + elif relative == "reference-graph-replay-frames.jsonl": + media_type = "application/x-ndjson" + schema_version = "missioncore.m48s-reference-graph-frame-evidence/v0" + role = "visual-evidence-full-replay-world-state" + artifacts.append( + { + "role": role, + "path": relative, + "byte_length": path.stat().st_size, + "sha256": sha256_path(path), + "media_type": media_type, + "schema_version": schema_version, + } + ) + return artifacts + + +def _read_object(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text("utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise M48SFixedClassDetectorLabError(f"invalid JSON evidence: {path.name}") from exc + if not isinstance(value, dict): + raise M48SFixedClassDetectorLabError(f"JSON evidence must be an object: {path.name}") + return value + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + try: + rows = [json.loads(line) for line in path.read_text("utf-8").splitlines() if line] + except (OSError, json.JSONDecodeError) as exc: + raise M48SFixedClassDetectorLabError("invalid YOLOX frame evidence") from exc + return _objects(rows, "YOLOX frame evidence") + + +def _objects(value: list[object], label: str) -> list[dict[str, Any]]: + if not all(isinstance(item, dict) for item in value): + raise M48SFixedClassDetectorLabError(f"{label} must contain objects") + return [item for item in value if isinstance(item, dict)] + + +__all__ = [ + "CATALOG_SCHEMA", + "FRAME_SCHEMA", + "LAB_SCHEMA", + "REPORT_SCHEMA", + "RESULT_PREFIX", + "M48SFixedClassDetectorLabError", + "M48SFixedClassDetectorLabResult", + "build_m48s_fixed_class_detector_lab", +] diff --git a/src/k1link/web/app.py b/src/k1link/web/app.py index d6e4049..f42f1d1 100644 --- a/src/k1link/web/app.py +++ b/src/k1link/web/app.py @@ -125,6 +125,9 @@ 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.m48s_fixed_class_detector_lab_api import ( + build_m48s_fixed_class_detector_lab_router, +) from k1link.web.map_api import ( MapGatewayConfiguration, MapGatewayProxy, @@ -944,6 +947,23 @@ app.include_router( ), ) ) +app.include_router( + build_m48s_fixed_class_detector_lab_router( + root_provider=lambda: ( + REPOSITORY_ROOT + / ".runtime" + / "compute-experiments" + / "m48s-semantic-shadow" + / "fixed-class-detector-lab-results" + ), + repository_root_provider=lambda: REPOSITORY_ROOT, + camera_frame_provider=( + session_recorded_camera_frame_service.extract + if session_recorded_camera_frame_service is not None + else None + ), + ) +) app.include_router( build_e47_semantic_slam_router( root_provider=lambda: ( diff --git a/src/k1link/web/m48s_fixed_class_detector_lab_api.py b/src/k1link/web/m48s_fixed_class_detector_lab_api.py new file mode 100644 index 0000000..e482be2 --- /dev/null +++ b/src/k1link/web/m48s_fixed_class_detector_lab_api.py @@ -0,0 +1,523 @@ +"""Read-only API for the sealed M4.8S fixed-class detector LAB.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import re +from collections.abc import Callable +from functools import lru_cache +from pathlib import Path, PurePosixPath +from typing import Any, Final + +from fastapi import APIRouter, HTTPException, Query, Response +from fastapi.responses import FileResponse + +from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition +from k1link.laboratory.evidence_report import ( + LaboratoryEvidenceReportError, + verify_laboratory_evidence_result, +) +from k1link.laboratory.m48s_fixed_class_detector_lab import ( + CATALOG_SCHEMA, + FRAME_SCHEMA, + INTEGRATED_STATUS, + LAB_SCHEMA, + RESULT_PREFIX, +) +from k1link.perception.fixed_class_detector_tournament import false_authority +from k1link.perception.m48s_replay_timeline import ( + M48sReplayTimeline, + M48sReplayTimelineError, +) +from k1link.perception.threat_timeline import RECORDED_SPATIAL_MAX_CHUNK_FRAMES +from k1link.sessions import RecordedCameraFrame, SessionIntegrityError + +RootProvider = Callable[[], Path | None] +CameraFrameProvider = Callable[[str, int], RecordedCameraFrame] + +RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$") +FRAME_ID: Final = re.compile(r"^[0-9]{6}$") +SHA256: Final = re.compile(r"^[a-f0-9]{64}$") +RESULT_PROJECTION_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-result-view/v1" +RESULT_CATALOG_SCHEMA: Final = "missioncore.m48s-fixed-class-detector-result-catalog/v1" +MAX_JSON_BYTES: Final = 16 * 1024 * 1024 +MAX_FRAMES: Final = 32 +_DEFINITION: Final = LaboratoryEvidenceDefinition( + work_id="m48s-fixed-class-detector", + runtime_relative_root=PurePosixPath("m48s-semantic-shadow/fixed-class-detector-lab-results"), + result_id_prefix="m48s-fixed-class-detector-lab", + document_name="manifest.json", + result_schema_version=LAB_SCHEMA, +) + + +def build_m48s_fixed_class_detector_lab_router( + *, + root_provider: RootProvider = lambda: None, + repository_root_provider: RootProvider = lambda: None, + camera_frame_provider: CameraFrameProvider | None = None, +) -> APIRouter: + router = APIRouter( + prefix="/api/v1/laboratory/m48s/fixed-class-detector", + tags=["laboratory"], + ) + + def timeline(result_id: str) -> M48sReplayTimeline: + candidate = _resolve_candidate(root_provider, result_id) + repository_root = _configured_root(repository_root_provider) + if repository_root is None: + raise HTTPException(status_code=503, detail="M4.8S timeline source unavailable") + try: + return _read_timeline_cached( + str(repository_root), + str(candidate), + result_id, + _timeline_signature(candidate), + ) + except (OSError, ValueError, M48sReplayTimelineError): + raise HTTPException( + status_code=503, + detail="M4.8S bounded replay timeline failed verification", + ) from None + + @router.get("/results") + def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]: + root = _configured_root(root_provider) + if root is None: + return _empty_catalog(False) + candidates = _candidates(root) + items: list[dict[str, object]] = [] + invalid_total = 0 + for candidate in candidates: + try: + items.append(_project_result(candidate)) + except RuntimeError: + invalid_total += 1 + items.sort( + key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])), + reverse=True, + ) + return { + "schema_version": RESULT_CATALOG_SCHEMA, + "configured": True, + "items": items[:limit], + "candidate_total": len(candidates), + "invalid_total": invalid_total, + "access": "read-only", + } + + @router.get("/{result_id}") + def get_result(result_id: str) -> dict[str, object]: + return _project_result(_resolve_candidate(root_provider, result_id)) + + @router.get("/{result_id}/frames/{frame_id}") + def get_frame(result_id: str, frame_id: str) -> dict[str, object]: + candidate, descriptor = _resolve_frame( + root_provider, + result_id=result_id, + frame_id=frame_id, + ) + path = candidate / str(descriptor["detail_path"]) + payload = _read_object(path) + if ( + payload.get("schema_version") != FRAME_SCHEMA + or payload.get("result_id") != result_id + or payload.get("frame_id") != frame_id + or descriptor.get("detail_sha256") != _sha256(path) + or descriptor.get("detail_byte_length") != path.stat().st_size + or not _valid_frame_payload(payload) + ): + raise HTTPException(status_code=404, detail="M4.8S frame not found") + return {**copy.deepcopy(payload), "access": "read-only"} + + @router.get("/{result_id}/frames/{frame_id}/camera") + def get_camera(result_id: str, frame_id: str) -> FileResponse: + candidate, descriptor = _resolve_frame( + root_provider, + result_id=result_id, + frame_id=frame_id, + ) + path = (candidate / str(descriptor["camera_path"])).resolve() + if ( + not path.is_relative_to(candidate) + or path.is_symlink() + or not path.is_file() + or descriptor.get("camera_sha256") != _sha256(path) + or descriptor.get("camera_byte_length") != path.stat().st_size + ): + raise HTTPException(status_code=404, detail="M4.8S camera not found") + return FileResponse(path, media_type="image/jpeg") + + @router.get("/{result_id}/timeline") + def get_timeline(result_id: str) -> dict[str, object]: + return copy.deepcopy(timeline(result_id).metadata()) + + @router.get("/{result_id}/timeline/chunk") + def get_timeline_chunk( + result_id: str, + start: int = Query(default=0, ge=0), + count: int = Query(default=12, ge=1, le=RECORDED_SPATIAL_MAX_CHUNK_FRAMES), + ) -> dict[str, object]: + try: + return timeline(result_id).chunk(start_sequence=start, frame_count=count) + except M48sReplayTimelineError: + raise HTTPException(status_code=404, detail="M4.8S timeline chunk not found") from None + + @router.get("/{result_id}/timeline/frames/{sequence}/camera") + def get_timeline_camera(result_id: str, sequence: int) -> Response: + if camera_frame_provider is None: + raise HTTPException(status_code=503, detail="M4.8S camera decoder unavailable") + projected = timeline(result_id) + if not 0 <= sequence < len(projected.source_times_ns): + raise HTTPException(status_code=404, detail="M4.8S timeline frame not found") + try: + camera = camera_frame_provider(projected.profile.session_id, sequence) + except (OSError, SessionIntegrityError, ValueError): + raise HTTPException(status_code=503, detail="M4.8S exact camera unavailable") from None + if camera.width != 800 or camera.height != 600: + raise HTTPException(status_code=503, detail="M4.8S camera size contract changed") + return Response( + content=camera.payload, + media_type=camera.media_type, + headers={ + "Cache-Control": "private, max-age=31536000, immutable", + "ETag": f'"{camera.sha256}"', + "X-Content-Type-Options": "nosniff", + }, + ) + + return router + + +@lru_cache(maxsize=4) +def _read_timeline_cached( + repository_root: str, + result_root: str, + result_id: str, + signature: tuple[int, ...], +) -> M48sReplayTimeline: + del signature + return M48sReplayTimeline( + repository_root=Path(repository_root), + result_root=Path(result_root), + result_id=result_id, + ) + + +def _timeline_signature(candidate: Path) -> tuple[int, ...]: + signature: list[int] = [] + for name in ( + "manifest.json", + "reference-graph-replay-frames.jsonl", + "reference-graph-replay-worker-result.json", + ): + path = candidate / name + if path.is_symlink() or not path.is_file(): + raise ValueError("M4.8S replay artifact is unavailable") + stat = path.stat() + signature.extend((stat.st_size, stat.st_mtime_ns)) + return tuple(signature) + + +def _project_result(candidate: Path) -> dict[str, object]: + loaded = _load_result(candidate) + manifest = loaded["manifest"] + catalog = loaded["catalog"] + identity = manifest["identity"] + return { + "schema_version": RESULT_PROJECTION_SCHEMA, + "result_id": manifest["result_id"], + "created_at_utc": manifest["created_at_utc"], + "status": manifest["status"], + "bounded_question_accepted": manifest["bounded_question_accepted"], + "ground_truth": manifest["ground_truth"], + "source": copy.deepcopy(identity["source"]), + "configuration": copy.deepcopy(identity["configuration"]), + "method": copy.deepcopy(manifest["method"]), + "metrics": copy.deepcopy(manifest["metrics"]), + "decision": copy.deepcopy(manifest["decision"]), + "limitations": copy.deepcopy(manifest["limitations"]), + "authority": copy.deepcopy(manifest["authority"]), + "frames": copy.deepcopy(catalog["frames"]), + "access": "read-only", + } + + +def _load_result(candidate: Path) -> dict[str, Any]: + try: + signature = _candidate_signature(candidate) + except (OSError, RuntimeError, ValueError) as exc: + raise RuntimeError("M4.8S result signature failed") from exc + return _load_result_cached(str(candidate), signature) + + +@lru_cache(maxsize=8) +def _load_result_cached(candidate_value: str, signature: tuple[int, ...]) -> dict[str, Any]: + del signature + return _load_result_uncached(Path(candidate_value)) + + +def _load_result_uncached(candidate: Path) -> dict[str, Any]: + if ( + not candidate.is_dir() + or candidate.is_symlink() + or RESULT_ID.fullmatch(candidate.name) is None + ): + raise RuntimeError("M4.8S result candidate is invalid") + try: + verify_laboratory_evidence_result(_DEFINITION, candidate) + except LaboratoryEvidenceReportError as exc: + raise RuntimeError("M4.8S result integrity failed") from exc + manifest = _read_object(candidate / "manifest.json") + identity = manifest.get("identity") + catalog_descriptor = manifest.get("catalog") + decision = manifest.get("decision") + method = manifest.get("method") + metrics = manifest.get("metrics") + status = manifest.get("status") + integrated = status == INTEGRATED_STATUS + if ( + manifest.get("schema_version") != LAB_SCHEMA + or manifest.get("result_id") != candidate.name + or status + not in { + "detector-load-passed-reference-graph-shadow-only", + INTEGRATED_STATUS, + } + or manifest.get("completed") is not True + or manifest.get("bounded_question_accepted") is not True + or manifest.get("ground_truth") is not False + or not isinstance(manifest.get("created_at_utc"), str) + or not isinstance(identity, dict) + or identity.get("schema_version") != LAB_SCHEMA + or manifest.get("identity_sha256") != hashlib.sha256(_canonical_json(identity)).hexdigest() + or not candidate.name.endswith(str(manifest.get("identity_sha256"))) + or identity.get("authority") != false_authority() + or manifest.get("authority") != false_authority() + or not isinstance(decision, dict) + or decision.get("bounded_question_accepted") is not True + or decision.get("ready_for_reference_graph_shadow") is not True + or decision.get("integrated_world_state_gate_evaluated") is not integrated + or (integrated and decision.get("integrated_world_state_gate_passed") is not True) + or (integrated and decision.get("detector_replacement_authorized") is not False) + or decision.get("production_accepted") is not False + or not isinstance(method, dict) + or method.get("schema_version") != "missioncore.laboratory-method/v1" + or method.get("completeness") != "complete" + or not isinstance(metrics, dict) + or (integrated and not isinstance(metrics.get("integrated_world_state"), dict)) + or not isinstance(manifest.get("limitations"), list) + or not isinstance(catalog_descriptor, dict) + or catalog_descriptor.get("path") != "catalog.json" + ): + raise RuntimeError("M4.8S manifest is invalid") + catalog_path = candidate / "catalog.json" + if ( + catalog_descriptor.get("sha256") != _sha256(catalog_path) + or catalog_descriptor.get("byte_length") != catalog_path.stat().st_size + ): + raise RuntimeError("M4.8S catalog changed") + catalog = _read_object(catalog_path) + frames = catalog.get("frames") + if ( + catalog.get("schema_version") != CATALOG_SCHEMA + or catalog.get("result_id") != candidate.name + or not isinstance(frames, list) + or not 1 <= len(frames) <= MAX_FRAMES + or catalog.get("frame_count") != len(frames) + or len({item.get("frame_id") for item in frames if isinstance(item, dict)}) != len(frames) + or any(not _valid_descriptor(item) for item in frames) + ): + raise RuntimeError("M4.8S catalog is invalid") + return {"manifest": manifest, "catalog": catalog} + + +def _candidate_signature(candidate: Path) -> tuple[int, ...]: + if not candidate.is_dir() or candidate.is_symlink(): + raise RuntimeError("M4.8S result candidate is invalid") + manifest_path = candidate / "manifest.json" + manifest = _read_object(manifest_path) + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list): + raise RuntimeError("M4.8S artifact manifest is invalid") + signature = [manifest_path.stat().st_size, manifest_path.stat().st_mtime_ns] + for descriptor in artifacts: + if not isinstance(descriptor, dict) or not isinstance(descriptor.get("path"), str): + raise RuntimeError("M4.8S artifact descriptor is invalid") + path = (candidate / descriptor["path"]).resolve(strict=True) + if not path.is_relative_to(candidate) or path.is_symlink() or not path.is_file(): + raise RuntimeError("M4.8S artifact path is invalid") + stat = path.stat() + signature.extend((stat.st_size, stat.st_mtime_ns)) + return tuple(signature) + + +def _resolve_candidate(root_provider: RootProvider, result_id: str) -> Path: + if RESULT_ID.fullmatch(result_id) is None: + raise HTTPException(status_code=404, detail="M4.8S result not found") + root = _configured_root(root_provider) + if root is None: + raise HTTPException(status_code=404, detail="M4.8S result not found") + candidate = root / result_id + try: + _load_result(candidate) + except RuntimeError: + raise HTTPException(status_code=404, detail="M4.8S result not found") from None + return candidate + + +def _resolve_frame( + root_provider: RootProvider, + *, + result_id: str, + frame_id: str, +) -> tuple[Path, dict[str, Any]]: + if FRAME_ID.fullmatch(frame_id) is None: + raise HTTPException(status_code=404, detail="M4.8S frame not found") + candidate = _resolve_candidate(root_provider, result_id) + loaded = _load_result(candidate) + try: + descriptor = next( + item for item in loaded["catalog"]["frames"] if item["frame_id"] == frame_id + ) + except StopIteration: + raise HTTPException(status_code=404, detail="M4.8S frame not found") from None + return candidate, descriptor + + +def _valid_descriptor(value: object) -> bool: + if not isinstance(value, dict): + return False + frame_id = value.get("frame_id") + counts = value.get("counts") + return ( + isinstance(frame_id, str) + and FRAME_ID.fullmatch(frame_id) is not None + and value.get("source_sequence") == int(frame_id) + and value.get("camera_path") == f"frames/frame-{frame_id}.jpg" + and isinstance(value.get("camera_sha256"), str) + and SHA256.fullmatch(value["camera_sha256"]) is not None + and isinstance(value.get("camera_byte_length"), int) + and value["camera_byte_length"] > 0 + and value.get("detail_path") == f"frame-{frame_id}.json" + and isinstance(value.get("detail_sha256"), str) + and SHA256.fullmatch(value["detail_sha256"]) is not None + and isinstance(value.get("detail_byte_length"), int) + and 0 < value["detail_byte_length"] <= MAX_JSON_BYTES + and isinstance(counts, dict) + and set(counts) == {"yolox", "dfine", "rf-detr"} + and all(isinstance(count, int) and count >= 0 for count in counts.values()) + ) + + +def _valid_frame_payload(value: dict[str, Any]) -> bool: + camera = value.get("camera") + detections = value.get("detections") + return ( + isinstance(value.get("source_sequence"), int) + and isinstance(camera, dict) + and camera.get("media_type") == "image/jpeg" + and camera.get("width") == 800 + and camera.get("height") == 600 + and camera.get("exact_source_frame") is True + and isinstance(camera.get("sha256"), str) + and SHA256.fullmatch(camera["sha256"]) is not None + and value.get("comparison_threshold") == 0.5 + and isinstance(detections, dict) + and set(detections) == {"yolox", "dfine", "rf-detr"} + and all( + isinstance(items, list) and all(_valid_detection(item) for item in items) + for items in detections.values() + ) + and value.get("ground_truth_available") is False + and value.get("authority") == false_authority() + ) + + +def _valid_detection(value: object) -> bool: + if not isinstance(value, dict) or set(value) != {"label", "score", "bbox_xyxy"}: + return False + score = value.get("score") + bbox = value.get("bbox_xyxy") + return ( + isinstance(value.get("label"), str) + and isinstance(score, (int, float)) + and not isinstance(score, bool) + and 0.5 <= float(score) <= 1.0 + and isinstance(bbox, list) + and len(bbox) == 4 + and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in bbox) + and 0 <= float(bbox[0]) < float(bbox[2]) <= 800 + and 0 <= float(bbox[1]) < float(bbox[3]) <= 600 + ) + + +def _configured_root(provider: RootProvider) -> Path | None: + value = provider() + if value is None: + return None + candidate = value.expanduser().absolute() + if candidate.is_symlink(): + return None + try: + resolved = candidate.resolve(strict=True) + except OSError: + return None + return resolved if resolved.is_dir() else None + + +def _candidates(root: Path) -> list[Path]: + return [ + item + for item in root.iterdir() + if item.is_dir() and not item.is_symlink() and RESULT_ID.fullmatch(item.name) + ] + + +def _empty_catalog(configured: bool) -> dict[str, object]: + return { + "schema_version": RESULT_CATALOG_SCHEMA, + "configured": configured, + "items": [], + "candidate_total": 0, + "invalid_total": 0, + "access": "read-only", + } + + +def _read_object(path: Path) -> dict[str, Any]: + if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_JSON_BYTES: + raise RuntimeError("M4.8S JSON artifact is invalid") + try: + value = json.loads(path.read_text("utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError("M4.8S JSON artifact is invalid") from exc + if not isinstance(value, dict): + raise RuntimeError("M4.8S JSON artifact is invalid") + return value + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _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() + + +__all__ = [ + "RESULT_CATALOG_SCHEMA", + "RESULT_PROJECTION_SCHEMA", + "build_m48s_fixed_class_detector_lab_router", +] diff --git a/tests/test_laboratory_evidence_registry.py b/tests/test_laboratory_evidence_registry.py index 92eafd3..8d0c737 100644 --- a/tests/test_laboratory_evidence_registry.py +++ b/tests/test_laboratory_evidence_registry.py @@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None: repository_root / "config" / "laboratories" ) - assert len(registry.definitions) == 36 + assert len(registry.definitions) == 37 assert {item.work_id for item in registry.definitions} >= { "e31-source-binding", "e46j-raw-fisheye-realtime", @@ -141,6 +141,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None: "m47-reference-graph-shadow", "m48-object-centric-quality", "m48-small-static-passage-regression", + "m48s-fixed-class-detector", } m48 = next( item for item in registry.definitions diff --git a/tests/test_laboratory_execution.py b/tests/test_laboratory_execution.py index 18e26aa..6bea79f 100644 --- a/tests/test_laboratory_execution.py +++ b/tests/test_laboratory_execution.py @@ -98,6 +98,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None: "e35-degradation-recovery", "e46j-raw-fisheye-realtime", "e47-semantic-slam-shadow", + "m48s-fixed-class-detector", } by_work_id = {row.work_id: row for row in execution.definitions} assert by_work_id["m48-small-static-passage-regression"].evidence_contract == ( @@ -108,10 +109,12 @@ def test_repository_registry_classifies_every_evidence_definition() -> None: ) assert by_work_id["e47-semantic-slam-shadow"].lifecycle == "experimental" assert by_work_id["e47-semantic-slam-shadow"].isolation == "bounded-adapter" + assert by_work_id["m48s-fixed-class-detector"].lifecycle == "experimental" + assert by_work_id["m48s-fixed-class-detector"].isolation == "bounded-adapter" assert all( row.lifecycle == "canonical" for row in execution.definitions - if row.work_id != "e47-semantic-slam-shadow" + if row.work_id not in {"e47-semantic-slam-shadow", "m48s-fixed-class-detector"} ) assert len(execution.definitions) + len(execution.legacy_work_ids) == len( evidence.definitions diff --git a/tests/test_laboratory_value_review_registry.py b/tests/test_laboratory_value_review_registry.py index 3f5b271..8081e99 100644 --- a/tests/test_laboratory_value_review_registry.py +++ b/tests/test_laboratory_value_review_registry.py @@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() -> root / "config" / "laboratory-value-review.json" ) - assert len(registry.entries) == 35 + assert len(registry.entries) == 36 assert {entry.catalog_id for entry in registry.entries} >= { "e28-local-surface", "e46d-temporal-failure-audit", @@ -88,4 +88,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() -> "l31-pointpillars-ravnoves", "l34f-adjudicated-reference", "m4-replay-threat", + "m48s-fixed-class-detector", } diff --git a/tests/test_m48s_fixed_class_detector_lab.py b/tests/test_m48s_fixed_class_detector_lab.py new file mode 100644 index 0000000..72eba14 --- /dev/null +++ b/tests/test_m48s_fixed_class_detector_lab.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from k1link.laboratory import LaboratoryEvidenceRegistry +from k1link.laboratory.evidence_report import verify_laboratory_evidence_result +from k1link.laboratory.m48s_fixed_class_detector_lab import ( + FRAME_IDS, + LAB_SCHEMA, + M48SFixedClassDetectorLabError, + build_m48s_fixed_class_detector_lab, +) + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +def test_m48s_lab_seals_visual_comparison_and_load_evidence(tmp_path: Path) -> None: + result = build_m48s_fixed_class_detector_lab( + repository_root=REPOSITORY_ROOT, + output_root=tmp_path / "results", + ) + manifest = result.manifest + identity = manifest["identity"] + identity_digest = hashlib.sha256( + json.dumps( + identity, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + + assert manifest["schema_version"] == LAB_SCHEMA + assert manifest["result_id"] == result.result_id + assert result.result_id.endswith(identity_digest) + assert manifest["identity_sha256"] == identity_digest + assert len(manifest["artifacts"]) == 31 + assert manifest["method"]["completeness"] == "complete" + assert manifest["bounded_question_accepted"] is True + assert manifest["ground_truth"] is False + assert manifest["decision"] == { + "bounded_question_accepted": True, + "selected_candidate": "rf-detr", + "ready_for_reference_graph_shadow": True, + "integrated_world_state_gate_evaluated": True, + "integrated_world_state_gate_passed": True, + "full_replay_visual_published": True, + "detector_replacement_authorized": False, + "production_accepted": False, + } + assert manifest["authority"]["navigation_or_safety_accepted"] is False + assert manifest["metrics"]["detector_load"]["source_frames_consumed"] == 18_008 + assert manifest["metrics"]["detector_load"]["source_frame_replacements"] == 0 + assert manifest["metrics"]["detector_load"]["effective_consumed_fps"] >= 9.5 + assert manifest["metrics"]["detector_load"]["completion_age_p95_ms"] <= 175.0 + integrated = manifest["metrics"]["integrated_world_state"] + assert integrated["source_frames_admitted"] == 4_489 + assert integrated["delivered_world_states"] == 4_481 + assert integrated["superseded_frames"] == 8 + assert integrated["effective_world_state_fps"] >= 9.5 + assert integrated["world_state_completion_age_p95_ms"] <= 175.0 + assert max(integrated["queue_high_watermarks"].values()) <= 2 + assert integrated["additional_inference_passes"] == 0 + assert integrated["failures"] == 0 + + catalog = json.loads((result.result_root / "catalog.json").read_text("utf-8")) + assert catalog["frame_count"] == len(FRAME_IDS) == 11 + frame = json.loads((result.result_root / "frame-000253.json").read_text("utf-8")) + assert not any(item["label"] == "dog" for item in frame["detections"]["yolox"]) + assert not any(item["label"] == "dog" for item in frame["detections"]["dfine"]) + assert any(item["label"] == "skateboard" for item in frame["detections"]["dfine"]) + assert [item["score"] for item in frame["detections"]["rf-detr"] if item["label"] == "dog"] == [ + 0.741674 + ] + + registry = LaboratoryEvidenceRegistry.from_directory( + REPOSITORY_ROOT / "config" / "laboratories" + ) + definition = next( + item for item in registry.definitions if item.work_id == "m48s-fixed-class-detector" + ) + proof = verify_laboratory_evidence_result(definition, result.result_root) + assert proof["result_id"] == result.result_id + assert proof["artifact_count"] == 31 + + with pytest.raises(M48SFixedClassDetectorLabError, match="already exists"): + build_m48s_fixed_class_detector_lab( + repository_root=REPOSITORY_ROOT, + output_root=tmp_path / "results", + ) diff --git a/tests/test_m48s_fixed_class_detector_lab_api.py b/tests/test_m48s_fixed_class_detector_lab_api.py new file mode 100644 index 0000000..ff86227 --- /dev/null +++ b/tests/test_m48s_fixed_class_detector_lab_api.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from pathlib import Path + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from k1link.laboratory.m48s_fixed_class_detector_lab import ( + build_m48s_fixed_class_detector_lab, +) +from k1link.web.m48s_fixed_class_detector_lab_api import ( + build_m48s_fixed_class_detector_lab_router, +) + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +def _fixture(tmp_path: Path) -> tuple[TestClient, Path, str]: + root = tmp_path / "results" + result = build_m48s_fixed_class_detector_lab( + repository_root=REPOSITORY_ROOT, + output_root=root, + ) + app = FastAPI() + app.include_router( + build_m48s_fixed_class_detector_lab_router( + root_provider=lambda: root, + repository_root_provider=lambda: REPOSITORY_ROOT, + ) + ) + return TestClient(app), result.result_root, result.result_id + + +def test_m48s_lab_api_projects_verified_result_frame_and_camera(tmp_path: Path) -> None: + client, result_root, result_id = _fixture(tmp_path) + + catalog = client.get("/api/v1/laboratory/m48s/fixed-class-detector/results") + assert catalog.status_code == 200 + assert catalog.json()["items"][0]["result_id"] == result_id + assert catalog.json()["invalid_total"] == 0 + + result = client.get(f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}") + assert result.status_code == 200 + assert result.json()["decision"]["selected_candidate"] == "rf-detr" + assert result.json()["decision"]["integrated_world_state_gate_passed"] is True + assert ( + result.json()["metrics"]["integrated_world_state"]["world_state_completion_age_p95_ms"] + == 74.733648 + ) + assert result.json()["ground_truth"] is False + assert len(result.json()["frames"]) == 11 + + timeline = client.get( + f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}/timeline" + ) + assert timeline.status_code == 200 + assert timeline.json()["frame_count"] == 4_489 + assert timeline.json()["world_state_frame_count"] == 4_481 + assert timeline.json()["superseded_frame_count"] == 8 + assert ( + timeline.json()["camera_point_delivery"] + == "factory-kb4-projected-current-increment" + ) + chunk = client.get( + f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}/timeline/chunk", + params={"start": 253, "count": 1}, + ) + assert chunk.status_code == 200 + replay_frame = chunk.json()["frames"][0] + assert replay_frame["world_state_available"] is True + assert replay_frame["camera_projection"] == "factory-kb4-exact" + assert replay_frame["camera_projected_sample_count"] > 0 + assert any( + item["semantic_hint"] == "dog" for item in replay_frame["camera_proposals"] + ) + + frame = client.get(f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}/frames/000253") + assert frame.status_code == 200 + assert any(item["label"] == "dog" for item in frame.json()["detections"]["rf-detr"]) + assert frame.json()["access"] == "read-only" + + camera = client.get( + f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}/frames/000253/camera" + ) + assert camera.status_code == 200 + assert camera.headers["content-type"] == "image/jpeg" + assert camera.content == (result_root / "frames/frame-000253.jpg").read_bytes() + + assert ( + client.get( + f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}/frames/not-a-frame" + ).status_code + == 404 + ) + assert ( + client.get("/api/v1/laboratory/m48s/fixed-class-detector/not-a-result").status_code == 404 + ) + + +def test_m48s_lab_api_fails_closed_after_artifact_tamper(tmp_path: Path) -> None: + client, result_root, result_id = _fixture(tmp_path) + (result_root / "frame-000253.json").write_text("{}\n", encoding="utf-8") + + response = client.get(f"/api/v1/laboratory/m48s/fixed-class-detector/{result_id}") + assert response.status_code == 404 + + catalog = client.get("/api/v1/laboratory/m48s/fixed-class-detector/results") + assert catalog.json()["items"] == [] + assert catalog.json()["invalid_total"] == 1