feat(lab): publish fail-closed TGS evidence
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
"""Seal and verify bounded gravity-aligned TRAVEL TGS evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
M49_TGS_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-result/v1"
|
||||
M49_TGS_REPORT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-report/v1"
|
||||
M49_TGS_WORKER_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-evidence-result/v1"
|
||||
M49_TGS_PREFIX: Final = "m49-tgs-fail-closed-"
|
||||
M49_TGS_PROFILE_ID: Final = "m49-ravnoves00-tgs-fail-closed-evidence/v1"
|
||||
M49_TGS_ANCHORS: Final = (171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856)
|
||||
M49_TGS_PROFILES: Final = ("current_increment", "causal_rolling_1s")
|
||||
_MAX_JSON_BYTES: Final = 1024 * 1024
|
||||
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||
|
||||
|
||||
class M49TgsFailClosedError(RuntimeError):
|
||||
"""The TGS evidence pack is unavailable or failed its immutable contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M49TgsFailClosedResult:
|
||||
result_id: str
|
||||
root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
@property
|
||||
def evidence_path(self) -> Path:
|
||||
return self.root / "evidence.npz"
|
||||
|
||||
|
||||
def seal_m49_tgs_fail_closed(
|
||||
*,
|
||||
source_root: Path,
|
||||
destination_root: Path,
|
||||
profile_path: Path,
|
||||
linked_visual_result_id: str,
|
||||
created_at_utc: str | None = None,
|
||||
) -> M49TgsFailClosedResult:
|
||||
source = _real_directory(source_root, "M49 Worker evidence")
|
||||
destination = destination_root.expanduser().absolute()
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if destination.is_symlink():
|
||||
raise M49TgsFailClosedError("M49 destination must not be a symlink")
|
||||
profile = _json_file(profile_path, "M49 profile")
|
||||
worker_result = _json_file(source / "result.json", "M49 Worker result")
|
||||
worker_summary = _json_file(source / "worker-summary.json", "M49 Worker summary")
|
||||
input_manifest = _json_file(source / "input-manifest.json", "M49 input manifest")
|
||||
timing = _timing_metrics(source / "tgs-timing.tsv")
|
||||
_validate_source(profile, worker_result, worker_summary, input_manifest, source)
|
||||
if (
|
||||
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 linked visual result is invalid")
|
||||
|
||||
evidence_sha = _file_sha256(source / "evidence.npz")
|
||||
identity = {
|
||||
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"source_pack_sha256": worker_result["source_pack_sha256"],
|
||||
"input_manifest_sha256": worker_result["input_manifest_sha256"],
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"anchor_frame_indices": list(M49_TGS_ANCHORS),
|
||||
},
|
||||
"configuration": {
|
||||
"profile_id": M49_TGS_PROFILE_ID,
|
||||
"config_sha256": worker_result["config_sha256"],
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"primary_profile": "causal_rolling_1s",
|
||||
"cell_size_m": worker_result["costmap"]["cell_size_m"],
|
||||
"radius_m": worker_result["costmap"]["radius_m"],
|
||||
},
|
||||
"method": {
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "travel-tgs-gravity-aligned-fail-closed/v1",
|
||||
"travel_revision": profile["source"]["travel_revision"],
|
||||
"aos_used": False,
|
||||
"missing_support_means_free": False,
|
||||
"eligible_point_accounting": "exact-multiset-complement",
|
||||
},
|
||||
"evidence": {
|
||||
"sha256": evidence_sha,
|
||||
"byte_length": (source / "evidence.npz").stat().st_size,
|
||||
"state_codes": profile["state_codes"],
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"{M49_TGS_PREFIX}{identity_sha256}"
|
||||
target = destination / result_id
|
||||
if target.exists():
|
||||
return read_m49_tgs_fail_closed(target)
|
||||
|
||||
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||
anchors = worker_result["anchors"]
|
||||
primary = [row for row in anchors if row["profile_id"] == "causal_rolling_1s"]
|
||||
report = {
|
||||
"schema_version": M49_TGS_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"source": identity["source"],
|
||||
"configuration": {
|
||||
**identity["configuration"],
|
||||
"state_priority": profile["costmap"]["state_priority"],
|
||||
"tgs": profile["tgs"],
|
||||
},
|
||||
"method": {
|
||||
**identity["method"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "TRAVEL GroundSeg",
|
||||
"version": profile["source"]["travel_revision"],
|
||||
"role": "gravity-aligned ground/nonground separation",
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "fail-closed complement adapter",
|
||||
"version": "v1",
|
||||
"role": "retain rejected points and explicit unobserved cells",
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "Worker 006 CPU qualification",
|
||||
"version": worker_summary["code_revision"],
|
||||
"role": "20 bounded TGS invocations without GPU",
|
||||
},
|
||||
],
|
||||
},
|
||||
"execution": {
|
||||
"worker": "Worker 006",
|
||||
"device": "cpu",
|
||||
"gpu_used": False,
|
||||
"wrapper_elapsed_seconds": worker_summary["wall_seconds"],
|
||||
"canonical_triton_id": worker_summary["canonical_triton_id"],
|
||||
"canonical_triton_health": worker_summary["canonical_triton_health"],
|
||||
"free_memory_gib_before": worker_summary["free_memory_gib_before"],
|
||||
},
|
||||
"metrics": {
|
||||
"anchor_count": len(M49_TGS_ANCHORS),
|
||||
"anchor_profile_count": len(anchors),
|
||||
"all_eligible_points_accounted": True,
|
||||
"primary_profile": "causal_rolling_1s",
|
||||
"primary": primary,
|
||||
"costmap_cell_count": worker_result["costmap"]["cell_count"],
|
||||
"process_wall_current_p50_ms": timing["current_increment"]["p50_ms"],
|
||||
"process_wall_current_max_ms": timing["current_increment"]["max_ms"],
|
||||
"process_wall_rolling_p50_ms": timing["causal_rolling_1s"]["p50_ms"],
|
||||
"process_wall_rolling_max_ms": timing["causal_rolling_1s"]["max_ms"],
|
||||
"process_max_rss_kib": timing["max_rss_kib"],
|
||||
},
|
||||
"acceptance": {
|
||||
"representation_complete": True,
|
||||
"all_points_accounted": True,
|
||||
"aos_absent": True,
|
||||
"gpu_absent": True,
|
||||
"visual_quality_accepted": False,
|
||||
"traversability_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"state": "visual-review-required",
|
||||
"candidate_retained": True,
|
||||
"next_action": (
|
||||
"Review exact gravity-aligned points and fail-closed costmap "
|
||||
"on the ten immutable anchors."
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"The ten anchors are bounded diagnostic evidence, not a full 4,489-frame replay.",
|
||||
"No independent terrain or traversability truth is available.",
|
||||
"No vehicle envelope exists, so occupied cells do not grant or deny physical passage.",
|
||||
"Visual quality, realtime integration, navigation and actuation remain unaccepted.",
|
||||
],
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
"visual_review": {
|
||||
"instrument": "m4-canonical-reference-graph",
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"anchors": list(M49_TGS_ANCHORS),
|
||||
"profiles": list(M49_TGS_PROFILES),
|
||||
"default_profile": "causal_rolling_1s",
|
||||
"point_states": profile["state_codes"],
|
||||
},
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-", dir=destination) as raw:
|
||||
staging = Path(raw) / result_id
|
||||
staging.mkdir()
|
||||
for name in (
|
||||
"evidence.npz",
|
||||
"worker-summary.json",
|
||||
"input-manifest.json",
|
||||
"tgs-timing.tsv",
|
||||
):
|
||||
shutil.copyfile(source / name, staging / name)
|
||||
_write_json(staging / "report.json", report)
|
||||
artifacts = [
|
||||
_artifact(staging / "report.json", "report", M49_TGS_REPORT_SCHEMA, "application/json"),
|
||||
_artifact(
|
||||
staging / "evidence.npz", "visual-spatial-evidence", None, "application/x-npz"
|
||||
),
|
||||
_artifact(staging / "worker-summary.json", "runtime-summary", None, "application/json"),
|
||||
_artifact(staging / "input-manifest.json", "source-manifest", None, "application/json"),
|
||||
_artifact(
|
||||
staging / "tgs-timing.tsv", "runtime-timing", None, "text/tab-separated-values"
|
||||
),
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"artifacts": artifacts,
|
||||
"authority": report["authority"],
|
||||
"ground_truth": False,
|
||||
}
|
||||
_write_json(staging / "manifest.json", manifest)
|
||||
os.replace(staging, target)
|
||||
return read_m49_tgs_fail_closed(target)
|
||||
|
||||
|
||||
def read_m49_tgs_fail_closed(root: Path) -> M49TgsFailClosedResult:
|
||||
candidate = _real_directory(root, "M49 result")
|
||||
if not candidate.name.startswith(M49_TGS_PREFIX):
|
||||
raise M49TgsFailClosedError("M49 result identity is invalid")
|
||||
manifest = _json_file(candidate / "manifest.json", "M49 manifest")
|
||||
report = _json_file(candidate / "report.json", "M49 report")
|
||||
if (
|
||||
manifest.get("schema_version") != M49_TGS_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or report.get("schema_version") != M49_TGS_REPORT_SCHEMA
|
||||
or report.get("result_id") != candidate.name
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 result contract changed")
|
||||
identity = manifest.get("identity")
|
||||
identity_sha = manifest.get("identity_sha256")
|
||||
if (
|
||||
not isinstance(identity, dict)
|
||||
or not isinstance(identity_sha, str)
|
||||
or _canonical_sha256(identity) != identity_sha
|
||||
or candidate.name != f"{M49_TGS_PREFIX}{identity_sha}"
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 identity proof changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||
raise M49TgsFailClosedError("M49 artifact manifest changed")
|
||||
for item in artifacts:
|
||||
if not isinstance(item, dict):
|
||||
raise M49TgsFailClosedError("M49 artifact descriptor changed")
|
||||
path = candidate / str(item.get("path", ""))
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.parent != candidate
|
||||
or path.stat().st_size != item.get("byte_length")
|
||||
or _file_sha256(path) != item.get("sha256")
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 artifact proof changed")
|
||||
return M49TgsFailClosedResult(candidate.name, candidate, manifest, report)
|
||||
|
||||
|
||||
def _validate_source(
|
||||
profile: dict[str, Any],
|
||||
worker_result: dict[str, Any],
|
||||
worker_summary: dict[str, Any],
|
||||
input_manifest: dict[str, Any],
|
||||
source: Path,
|
||||
) -> None:
|
||||
if (
|
||||
profile.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
|
||||
or profile.get("profile_id") != M49_TGS_PROFILE_ID
|
||||
or tuple(profile.get("anchors", ())) != M49_TGS_ANCHORS
|
||||
or profile.get("invariants", {}).get("aos_allowed") is not False
|
||||
or profile.get("invariants", {}).get("missing_support_means_free") is not False
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 profile changed")
|
||||
if (
|
||||
worker_result.get("schema_version") != M49_TGS_WORKER_RESULT_SCHEMA
|
||||
or worker_result.get("status") != "passed"
|
||||
or worker_result.get("summary", {}).get("aos_used") is not False
|
||||
or worker_result.get("summary", {}).get("all_eligible_points_accounted") is not True
|
||||
or worker_result.get("summary", {}).get("primary_profile") != "causal_rolling_1s"
|
||||
or len(worker_result.get("anchors", ())) != 20
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker result changed")
|
||||
if (
|
||||
input_manifest.get("schema_version") != "missioncore.m49-tgs-fail-closed-input/v1"
|
||||
or input_manifest.get("coordinate_frame") != "map-gravity-local"
|
||||
or len(input_manifest.get("records", ())) != 20
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 input manifest changed")
|
||||
if (
|
||||
worker_summary.get("gpu_requested") is not False
|
||||
and worker_summary.get("gpu_requested") is not None
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker GPU contract changed")
|
||||
if (
|
||||
worker_summary.get("aos_used") is not False
|
||||
or worker_summary.get("all_eligible_points_accounted") is not True
|
||||
or worker_summary.get("canonical_triton_health") != "healthy"
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker cleanup changed")
|
||||
evidence = worker_result.get("evidence", {})
|
||||
if (
|
||||
evidence.get("path") != "evidence.npz"
|
||||
or evidence.get("bytes") != (source / "evidence.npz").stat().st_size
|
||||
or evidence.get("sha256") != _file_sha256(source / "evidence.npz")
|
||||
or worker_result.get("input_manifest_sha256")
|
||||
!= _file_sha256(source / "input-manifest.json")
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 evidence proof changed")
|
||||
|
||||
|
||||
def _artifact(
|
||||
path: Path,
|
||||
role: str,
|
||||
schema_version: str | None,
|
||||
media_type: str,
|
||||
) -> dict[str, object]:
|
||||
result: dict[str, object] = {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _file_sha256(path),
|
||||
"media_type": media_type,
|
||||
}
|
||||
if schema_version is not None:
|
||||
result["schema_version"] = schema_version
|
||||
return result
|
||||
|
||||
|
||||
def _timing_metrics(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise M49TgsFailClosedError("M49 timing evidence is unavailable")
|
||||
profiles: dict[str, list[float]] = {name: [] for name in M49_TGS_PROFILES}
|
||||
maximum_rss = 0
|
||||
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
||||
if not lines or lines[0] != "profile\tslot\twall_seconds\tmax_rss_kib":
|
||||
raise M49TgsFailClosedError("M49 timing evidence changed")
|
||||
for line in lines[1:]:
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 4 or fields[0] not in profiles:
|
||||
raise M49TgsFailClosedError("M49 timing row changed")
|
||||
profiles[fields[0]].append(float(fields[2]))
|
||||
maximum_rss = max(maximum_rss, int(fields[3]))
|
||||
if any(len(values) != len(M49_TGS_ANCHORS) for values in profiles.values()):
|
||||
raise M49TgsFailClosedError("M49 timing coverage changed")
|
||||
result: dict[str, Any] = {"max_rss_kib": maximum_rss}
|
||||
for name, values in profiles.items():
|
||||
ordered = sorted(values)
|
||||
result[name] = {
|
||||
"p50_ms": ordered[len(ordered) // 2] * 1000,
|
||||
"max_ms": max(ordered) * 1000,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _real_directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M49TgsFailClosedError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_dir():
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||
return resolved
|
||||
|
||||
|
||||
def _json_file(path: Path, label: str) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
raise M49TgsFailClosedError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise M49TgsFailClosedError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=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(_HASH_CHUNK_BYTES), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M49_TGS_ANCHORS",
|
||||
"M49_TGS_PREFIX",
|
||||
"M49_TGS_REPORT_SCHEMA",
|
||||
"M49_TGS_RESULT_SCHEMA",
|
||||
"M49TgsFailClosedError",
|
||||
"M49TgsFailClosedResult",
|
||||
"read_m49_tgs_fail_closed",
|
||||
"seal_m49_tgs_fail_closed",
|
||||
]
|
||||
@@ -133,6 +133,7 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
|
||||
build_m48s_fixed_class_detector_lab_router,
|
||||
)
|
||||
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||
from k1link.web.map_api import (
|
||||
MapGatewayConfiguration,
|
||||
MapGatewayProxy,
|
||||
@@ -992,6 +993,17 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m49_tgs_fail_closed_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "m49"
|
||||
/ "tgs-fail-closed-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m48s_fixed_class_detector_lab_router(
|
||||
root_provider=lambda: (
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Read-only API for sealed gravity-aligned M49 TGS evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException, Query, Response
|
||||
|
||||
from k1link.laboratory.m49_tgs_fail_closed import (
|
||||
M49_TGS_ANCHORS,
|
||||
M49_TGS_PREFIX,
|
||||
M49TgsFailClosedError,
|
||||
M49TgsFailClosedResult,
|
||||
read_m49_tgs_fail_closed,
|
||||
)
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
RESULT_ID: Final = re.compile(rf"^{re.escape(M49_TGS_PREFIX)}[a-f0-9]{{64}}$")
|
||||
RESULT_VIEW_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-view/v1"
|
||||
RESULT_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-catalog/v1"
|
||||
ANCHOR_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-anchor-catalog/v1"
|
||||
ANCHOR_SPATIAL_SCHEMA: Final = "missioncore.m49-tgs-anchor-spatial/v1"
|
||||
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-fail-closed"
|
||||
PROFILES: Final = ("current_increment", "causal_rolling_1s")
|
||||
|
||||
|
||||
def build_m49_tgs_fail_closed_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
|
||||
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||
|
||||
def result(result_id: str) -> M49TgsFailClosedResult:
|
||||
candidate = _resolve_candidate(root_provider, result_id)
|
||||
try:
|
||||
return _read_result_cached(str(candidate), _signature(candidate))
|
||||
except (M49TgsFailClosedError, OSError, ValueError):
|
||||
raise HTTPException(status_code=404, detail="M49 TGS result not found") 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(configured=False)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in sorted(root.iterdir()):
|
||||
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
|
||||
continue
|
||||
try:
|
||||
sealed = _read_result_cached(str(candidate.resolve()), _signature(candidate))
|
||||
items.append(_project_result(sealed))
|
||||
except (M49TgsFailClosedError, OSError, ValueError):
|
||||
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(items) + invalid_total,
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/{result_id}")
|
||||
def get_result(result_id: str) -> dict[str, object]:
|
||||
return _project_result(result(result_id))
|
||||
|
||||
@router.get("/{result_id}/anchors")
|
||||
def get_anchors(result_id: str) -> dict[str, object]:
|
||||
sealed = result(result_id)
|
||||
return {
|
||||
"schema_version": ANCHOR_CATALOG_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"linked_visual_result_id": sealed.report["visual_review"]["linked_visual_result_id"],
|
||||
"anchors": copy.deepcopy(sealed.report["metrics"]["primary"]),
|
||||
"anchor_count": len(M49_TGS_ANCHORS),
|
||||
"profiles": list(PROFILES),
|
||||
"default_profile": "causal_rolling_1s",
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/{result_id}/anchors/{anchor_frame_index}/spatial")
|
||||
def get_anchor_spatial(
|
||||
result_id: str,
|
||||
anchor_frame_index: int,
|
||||
profile: str = Query(default="causal_rolling_1s"),
|
||||
) -> Response:
|
||||
if profile not in PROFILES or anchor_frame_index not in M49_TGS_ANCHORS:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS anchor not found")
|
||||
sealed = result(result_id)
|
||||
try:
|
||||
content = _anchor_json_cached(
|
||||
str(sealed.evidence_path),
|
||||
result_id,
|
||||
anchor_frame_index,
|
||||
profile,
|
||||
_evidence_signature(sealed.evidence_path),
|
||||
)
|
||||
except (KeyError, OSError, ValueError):
|
||||
raise HTTPException(
|
||||
status_code=503, detail="M49 TGS spatial evidence failed verification"
|
||||
) from None
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _read_result_cached(result_root: str, signature: tuple[int, ...]) -> M49TgsFailClosedResult:
|
||||
del signature
|
||||
return read_m49_tgs_fail_closed(Path(result_root))
|
||||
|
||||
|
||||
@lru_cache(maxsize=24)
|
||||
def _anchor_json_cached(
|
||||
evidence_path: str,
|
||||
result_id: str,
|
||||
anchor_frame_index: int,
|
||||
profile: str,
|
||||
signature: tuple[int, int],
|
||||
) -> bytes:
|
||||
del signature
|
||||
slot = M49_TGS_ANCHORS.index(anchor_frame_index)
|
||||
with np.load(evidence_path, allow_pickle=False) as evidence:
|
||||
offsets = evidence[f"{profile}_point_offsets"]
|
||||
start = int(offsets[slot])
|
||||
end = int(offsets[slot + 1])
|
||||
points = evidence[f"{profile}_points_xyz_m"][start:end]
|
||||
point_states = evidence[f"{profile}_point_states"][start:end]
|
||||
centers = evidence["costmap_cell_centers_xy_m"]
|
||||
cell_states = evidence[f"{profile}_costmap_states"][slot]
|
||||
z_bounds = evidence[f"{profile}_costmap_z_bounds_m"][slot]
|
||||
if (
|
||||
points.shape[1:] != (3,)
|
||||
or point_states.shape != (points.shape[0],)
|
||||
or centers.shape != (2244, 2)
|
||||
or cell_states.shape != (2244,)
|
||||
or z_bounds.shape != (2244, 2)
|
||||
or not np.isfinite(points).all()
|
||||
or not np.isfinite(centers).all()
|
||||
or not np.isin(point_states, np.asarray([1, 2, 3], dtype=np.uint8)).all()
|
||||
or not np.isin(cell_states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
|
||||
):
|
||||
raise ValueError("M49 TGS spatial shape changed")
|
||||
payload = {
|
||||
"schema_version": ANCHOR_SPATIAL_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"anchor_frame_index": anchor_frame_index,
|
||||
"source_sequence": anchor_frame_index,
|
||||
"profile": profile,
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"points_xyz_m": points.astype(float).tolist(),
|
||||
"point_states": point_states.astype(int).tolist(),
|
||||
"costmap": {
|
||||
"cell_size_m": 0.45,
|
||||
"radius_m": 12.0,
|
||||
"centers_xy_m": centers.astype(float).tolist(),
|
||||
"states": cell_states.astype(int).tolist(),
|
||||
"z_bounds_m": [
|
||||
[
|
||||
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||
]
|
||||
for row in z_bounds
|
||||
],
|
||||
},
|
||||
"state_codes": {
|
||||
"UNOBSERVED": 0,
|
||||
"GROUND_SUPPORT": 1,
|
||||
"NONGROUND_OCCUPIED": 2,
|
||||
"UNKNOWN_REJECTED": 3,
|
||||
},
|
||||
"all_points_accounted": True,
|
||||
"aos_used": False,
|
||||
"authority": {
|
||||
"visual_quality_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"access": "read-only",
|
||||
}
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _project_result(result: M49TgsFailClosedResult) -> dict[str, object]:
|
||||
report = result.report
|
||||
return {
|
||||
"schema_version": RESULT_VIEW_SCHEMA,
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest["created_at_utc"],
|
||||
"source": copy.deepcopy(report["source"]),
|
||||
"configuration": copy.deepcopy(report["configuration"]),
|
||||
"method": copy.deepcopy(report["method"]),
|
||||
"execution": copy.deepcopy(report["execution"]),
|
||||
"metrics": copy.deepcopy(report["metrics"]),
|
||||
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||
"decision": copy.deepcopy(report["decision"]),
|
||||
"limitations": copy.deepcopy(report["limitations"]),
|
||||
"authority": copy.deepcopy(report["authority"]),
|
||||
"visual_review": copy.deepcopy(report["visual_review"]),
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
||||
if RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||
root = _configured_root(provider)
|
||||
if root is None:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||
candidate = root / result_id
|
||||
if candidate.is_symlink() or not candidate.is_dir():
|
||||
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||
resolved = candidate.resolve(strict=True)
|
||||
if resolved.parent != root:
|
||||
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||
return resolved
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None or value.is_symlink() or not value.is_dir():
|
||||
return None
|
||||
return value.resolve(strict=True)
|
||||
|
||||
|
||||
def _signature(candidate: Path) -> tuple[int, ...]:
|
||||
result: list[int] = []
|
||||
for name in (
|
||||
"manifest.json",
|
||||
"report.json",
|
||||
"evidence.npz",
|
||||
"worker-summary.json",
|
||||
"input-manifest.json",
|
||||
"tgs-timing.tsv",
|
||||
):
|
||||
path = candidate / name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("M49 TGS artifact unavailable")
|
||||
stat = path.stat()
|
||||
result.extend((stat.st_size, stat.st_mtime_ns))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _evidence_signature(path: Path) -> tuple[int, int]:
|
||||
stat = path.stat()
|
||||
return stat.st_size, stat.st_mtime_ns
|
||||
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["build_m49_tgs_fail_closed_router"]
|
||||
Reference in New Issue
Block a user