NODEDC_MISSION_CORE/src/k1link/web/polygon_api.py

907 lines
36 KiB
Python

from __future__ import annotations
import hashlib
import json
import math
import os
import re
import secrets
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import Annotated, Any, Final
import numpy as np
from fastapi import APIRouter, Header, HTTPException, Query
from fastapi import Path as PathParameter
from pydantic import BaseModel, ConfigDict, Field
from k1link.simulation import (
QualificationRun,
QualificationRunIntegrityError,
QualificationRunNotFoundError,
QualificationRunStore,
)
from k1link.simulation.worker_gateway import (
STATUS_SCHEMA,
PolygonWorkerGateway,
SimulationWorkerGatewayError,
SimulationWorkerRejectedError,
SimulationWorkerUnavailableError,
UnixSocketWorkerGateway,
)
POLYGON_RUNS_ROOT_ENV: Final = "MISSIONCORE_POLYGON_RUNS_ROOT"
POLYGON_REVIEW_ROOT_ENV: Final = "MISSIONCORE_POLYGON_REVIEW_ROOT"
POLYGON_WORKER_SOCKET_ENV: Final = "MISSIONCORE_POLYGON_WORKER_SOCKET"
POLYGON_WORKER_CONTROL_ENV: Final = "MISSIONCORE_POLYGON_WORKER_CONTROL"
MISSION_CORE_COMMIT_ENV: Final = "MISSIONCORE_COMMIT"
CATALOG_SCHEMA: Final = "missioncore.polygon-run-catalog/v1"
DETAIL_SCHEMA: Final = "missioncore.polygon-run-detail/v1"
GROUND_QUALIFICATION_SCHEMA: Final = "missioncore.polygon-ground-qualification/v1"
GROUND_FAILURE_PREVIEW_SCHEMA: Final = "missioncore.polygon-ground-failure-preview/v1"
GROUND_REPORT_ARTIFACT_KIND: Final = "goose-ground-qualification-report"
RELLIS_REPORT_ARTIFACT_KIND: Final = "rellis-ground-qualification-report"
GROUND_FAILURE_ARTIFACT_KIND: Final = "goose-ground-qualification-failure-preview"
GROUND_REPORT_SCHEMA: Final = "missioncore.goose-ground-qualification-report/v1"
RELLIS_REPORT_SCHEMA: Final = "missioncore.rellis-ground-qualification-report/v1"
GROUND_FAILURE_SCHEMA: Final = "missioncore.goose-ground-qualification-failure-preview/v1"
GROUND_REVIEW_SCHEMA: Final = "missioncore.polygon-ground-review/v2"
GROUND_REVIEW_FRAME_SCHEMA: Final = "missioncore.polygon-ground-review-frame/v1"
GOOSE_REVIEW_PACK_SCHEMA: Final = "missioncore.goose-ground-review-pack/v1"
GOOSE_REVIEW_FRAME_SCHEMA: Final = "missioncore.goose-ground-review-frame/v1"
RELLIS_REVIEW_PACK_SCHEMA: Final = "missioncore.rellis-ground-review-pack/v1"
RELLIS_REVIEW_FRAME_SCHEMA: Final = "missioncore.rellis-ground-review-frame/v1"
MAX_QUALIFICATION_ARTIFACT_BYTES: Final = 32 * 1024**2
MAX_REVIEW_MANIFEST_BYTES: Final = 8 * 1024**2
MAX_REVIEW_FRAME_BYTES: Final = 2 * 1024**2
MAX_REVIEW_FRAMES: Final = 3_000
MAX_REVIEW_POINTS: Final = 20_000
COMMIT_PATTERN: Final = re.compile(r"^[a-f0-9]{40}$")
GOOSE_FRAME_ID_PATTERN: Final = re.compile(
r"^(?P<dataset_sequence_id>[A-Za-z0-9][A-Za-z0-9_-]{0,95})"
r"__(?P<dataset_frame_number>[0-9]{4})"
r"_(?P<sensor_timestamp_ns>[0-9]{16,20})$"
)
READ_ONLY_LIMITATIONS: Final[tuple[str, ...]] = (
"Архивный UI-0 публикует только квалификационные доказательства; "
"содержимое команд и mutation-маршруты в архиве отсутствуют.",
"Артефакты представлены относительными метаданными; их содержимое не публикуется этим API.",
"Терминальное состояние прогона не означает приёмку навигации, "
"восприятия или физической безопасности.",
)
RootProvider = Callable[[], Path | None]
WorkerProvider = Callable[[], PolygonWorkerGateway | None]
ControlProvider = Callable[[], bool]
CommitProvider = Callable[[], str | None]
class StartStockRoverRequest(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
scenario_id: str
class AckermannCommandRequest(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
speed_mps: float = Field(ge=-1.5, le=1.5, allow_inf_nan=False)
steering_normalized: float = Field(ge=-1.0, le=1.0, allow_inf_nan=False)
def configured_polygon_runs_root() -> Path | None:
raw = os.environ.get(POLYGON_RUNS_ROOT_ENV)
if raw is None or not raw.strip():
return None
return Path(raw.strip()).expanduser()
def configured_polygon_worker() -> PolygonWorkerGateway | None:
raw = os.environ.get(POLYGON_WORKER_SOCKET_ENV)
if raw is None or not raw.strip():
return None
return UnixSocketWorkerGateway(Path(raw.strip()).expanduser())
def configured_polygon_control() -> bool:
return os.environ.get(POLYGON_WORKER_CONTROL_ENV, "").strip() == "internal-virtual-only"
def configured_mission_core_commit() -> str | None:
raw = os.environ.get(MISSION_CORE_COMMIT_ENV)
if raw is None:
return None
normalized = raw.strip()
return normalized if COMMIT_PATTERN.fullmatch(normalized) else None
def build_polygon_router(
*,
root_provider: RootProvider = configured_polygon_runs_root,
worker_provider: WorkerProvider = configured_polygon_worker,
control_provider: ControlProvider = configured_polygon_control,
commit_provider: CommitProvider = configured_mission_core_commit,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/polygon", tags=["polygon"])
@router.get("/runs")
def list_polygon_runs(
limit: Annotated[int, Query(ge=1, le=100)] = 20,
) -> dict[str, Any]:
store = _open_read_only_store(root_provider)
try:
ordered = sorted(
store.list_runs(),
key=lambda run: (run.created_at_utc, run.run_id),
reverse=True,
)
except QualificationRunIntegrityError as exc:
raise HTTPException(
status_code=500,
detail="Журнал прогонов Полигона не прошёл проверку целостности.",
) from exc
return {
"schema_version": CATALOG_SCHEMA,
"access": "read-only",
"items": [_run_summary(run) for run in ordered[:limit]],
"total": len(ordered),
"limitations": list(READ_ONLY_LIMITATIONS),
}
@router.get("/runs/{run_id}")
def get_polygon_run(
run_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
event_limit: Annotated[int, Query(ge=1, le=500)] = 200,
) -> dict[str, Any]:
store = _open_read_only_store(root_provider)
try:
run = store.load(run_id)
events = store.list_events(run_id)
commands = store.list_commands(run_id)
except QualificationRunNotFoundError as exc:
raise HTTPException(status_code=404, detail="Прогон Полигона не найден.") from exc
except QualificationRunIntegrityError as exc:
raise HTTPException(
status_code=500,
detail="Доказательства прогона не прошли проверку целостности.",
) from exc
visible_events = events[-event_limit:]
return {
"schema_version": DETAIL_SCHEMA,
"access": "read-only",
"run": {
**_run_summary(run),
"scenario_sha256": run.scenario_sha256,
"profile_sha256": run.profile_sha256,
"host_profile_sha256": run.host_profile_sha256,
"seed": run.seed,
"parent_run_id": run.parent_run_id,
"providers": [provider.to_dict() for provider in run.providers],
"authority": run.authority.to_dict(),
},
"events": [event.to_dict() for event in visible_events],
"events_total": len(events),
"events_truncated": len(visible_events) != len(events),
"commands": {
"count": len(commands),
"content_exposed": False,
},
"artifacts": [artifact.to_dict() for artifact in run.artifacts],
"limitations": list(READ_ONLY_LIMITATIONS),
}
@router.get("/runs/{run_id}/qualification")
def get_polygon_ground_qualification(
run_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
) -> dict[str, Any]:
store, run = _load_run(root_provider, run_id)
report = _read_registered_json(
store,
run,
artifact_kind=(GROUND_REPORT_ARTIFACT_KIND, RELLIS_REPORT_ARTIFACT_KIND),
expected_schema=(GROUND_REPORT_SCHEMA, RELLIS_REPORT_SCHEMA),
)
required = (
"identity_sha256",
"source_id",
"split",
"frame_count",
"aggregates",
"degradations",
"checks",
"worst_frames",
"decision",
"safety",
)
if any(key not in report for key in required):
raise HTTPException(
status_code=500,
detail="Отчёт квалификации имеет неполный контракт.",
)
return {
"schema_version": GROUND_QUALIFICATION_SCHEMA,
"access": "read-only",
"run_id": run.run_id,
**{key: report[key] for key in required},
}
@router.get("/runs/{run_id}/qualification/failures/{frame_id}")
def get_polygon_ground_failure_preview(
run_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
frame_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
) -> dict[str, Any]:
store, run = _load_run(root_provider, run_id)
preview = _read_registered_json(
store,
run,
artifact_kind=GROUND_FAILURE_ARTIFACT_KIND,
expected_schema=GROUND_FAILURE_SCHEMA,
frame_id=frame_id,
)
required = (
"source_id",
"frame_id",
"source_point_count",
"point_count",
"sampling",
"points_xyz_m",
"ground_truth_ground",
"evaluated",
"current_ground",
"patchwork_ground",
"current_disagreement",
"patchwork_disagreement",
"safety",
)
if any(key not in preview for key in required) or preview["frame_id"] != frame_id:
raise HTTPException(
status_code=500,
detail="Preview проблемного кадра имеет неполный контракт.",
)
return {
"schema_version": GROUND_FAILURE_PREVIEW_SCHEMA,
"access": "read-only",
"run_id": run.run_id,
**{key: preview[key] for key in required},
}
@router.get("/runs/{run_id}/qualification/review")
def get_polygon_ground_review(
run_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
) -> dict[str, Any]:
_, run = _load_run(root_provider, run_id)
_, manifest = _read_ground_review_manifest(root_provider, run.run_id)
return {
"schema_version": GROUND_REVIEW_SCHEMA,
"access": "read-only",
"run_id": run.run_id,
"identity_sha256": manifest["identity_sha256"],
"source_id": manifest["source_id"],
"frame_count": manifest["frame_count"],
"preview_points": manifest["preview_points"],
"frames": [
{
**{
key: frame[key]
for key in (
"sequence",
"frame_id",
"source_point_count",
"point_count",
"current",
"patchworkpp",
"ground_iou_delta",
)
},
**_dataset_frame_identity(frame),
}
for frame in manifest["frames"]
],
"safety": manifest["safety"],
}
@router.get("/runs/{run_id}/qualification/review/frames/{frame_id}")
def get_polygon_ground_review_frame(
run_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
frame_id: Annotated[
str,
PathParameter(pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"),
],
) -> dict[str, Any]:
_, run = _load_run(root_provider, run_id)
pack_root, manifest = _read_ground_review_manifest(root_provider, run.run_id)
matches = [frame for frame in manifest["frames"] if frame.get("frame_id") == frame_id]
if len(matches) != 1:
raise HTTPException(
status_code=404,
detail="Кадр визуального прогона не найден.",
)
frame = matches[0]
path = _verified_review_frame_path(pack_root, frame)
try:
with np.load(path, allow_pickle=False) as source:
schema = str(source["schema"][0])
stored_frame_id = str(source["frame_id"][0])
source_point_count = int(source["source_point_count"][0])
xyz_cm = np.asarray(source["xyz_cm"])
remission = np.asarray(source["remission_u8"])
flags = np.asarray(source["flags"])
except (OSError, ValueError, KeyError, IndexError) as exc:
raise HTTPException(
status_code=500,
detail="Кадр визуального прогона повреждён.",
) from exc
point_count = int(frame["point_count"])
if (
schema
not in {
GOOSE_REVIEW_FRAME_SCHEMA,
RELLIS_REVIEW_FRAME_SCHEMA,
}
or stored_frame_id != frame_id
or source_point_count != frame["source_point_count"]
or xyz_cm.dtype != np.dtype("<i2")
or xyz_cm.shape != (point_count, 3)
or remission.dtype != np.uint8
or remission.shape != (point_count,)
or flags.dtype != np.uint8
or flags.shape != (point_count,)
or not 1 <= point_count <= MAX_REVIEW_POINTS
or np.any(flags > 15)
):
raise HTTPException(
status_code=500,
detail="Кадр визуального прогона нарушает контракт.",
)
evaluated = (flags & 1) != 0
ground_truth = (flags & 2) != 0
current = (flags & 4) != 0
patchwork = (flags & 8) != 0
return {
"schema_version": GROUND_REVIEW_FRAME_SCHEMA,
"access": "read-only",
"run_id": run.run_id,
"source_id": manifest["source_id"],
"frame_id": frame_id,
"source_point_count": source_point_count,
"point_count": point_count,
"sampling": "deterministic-even-index",
"points_xyz_m": (xyz_cm.astype(np.float32) / 100.0).tolist(),
"intensity_0_255": remission.tolist(),
"ground_truth_ground": ground_truth.astype(np.uint8).tolist(),
"evaluated": evaluated.astype(np.uint8).tolist(),
"current_ground": current.astype(np.uint8).tolist(),
"patchwork_ground": patchwork.astype(np.uint8).tolist(),
"current_disagreement": (evaluated & (current != ground_truth))
.astype(np.uint8)
.tolist(),
"patchwork_disagreement": (evaluated & (patchwork != ground_truth))
.astype(np.uint8)
.tolist(),
"safety": manifest["safety"],
}
@router.get("/worker")
def get_polygon_worker() -> dict[str, Any]:
gateway = worker_provider()
if gateway is None:
return _unavailable_worker_status()
try:
status = gateway.status()
except SimulationWorkerGatewayError:
return _unavailable_worker_status()
return {
**status,
"control_available": bool(status["control_available"] and control_provider()),
}
@router.get("/worker/live")
def get_polygon_worker_live() -> dict[str, Any]:
gateway = _required_worker(worker_provider)
try:
return gateway.live()
except SimulationWorkerUnavailableError as exc:
raise HTTPException(
status_code=503,
detail="Simulation Worker недоступен.",
) from exc
except SimulationWorkerRejectedError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except SimulationWorkerGatewayError as exc:
raise HTTPException(
status_code=502,
detail="Simulation Worker вернул некорректное состояние.",
) from exc
@router.post("/worker/runs")
def start_polygon_worker_run(
request: StartStockRoverRequest,
idempotency_key: Annotated[
str,
Header(alias="Idempotency-Key", min_length=1, max_length=160),
],
) -> dict[str, Any]:
if request.scenario_id != "stock-rover-ackermann":
raise HTTPException(status_code=422, detail="Сценарий Полигона не поддерживается.")
gateway, commit = _control_context(
worker_provider,
control_provider,
commit_provider,
)
run_id = _new_run_id(commit)
try:
return gateway.start(
run_id=run_id,
mission_core_commit=commit,
idempotency_key=idempotency_key,
)
except SimulationWorkerUnavailableError as exc:
raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
except SimulationWorkerRejectedError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except SimulationWorkerGatewayError as exc:
raise HTTPException(
status_code=502,
detail="Simulation Worker не подтвердил запуск.",
) from exc
@router.post("/worker/runs/{run_id}/stop")
def stop_polygon_worker_run(
run_id: Annotated[
str,
PathParameter(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$"),
],
idempotency_key: Annotated[
str,
Header(alias="Idempotency-Key", min_length=1, max_length=160),
],
) -> dict[str, Any]:
gateway, _ = _control_context(
worker_provider,
control_provider,
commit_provider,
)
try:
return gateway.stop(run_id=run_id, idempotency_key=idempotency_key)
except SimulationWorkerUnavailableError as exc:
raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
except SimulationWorkerRejectedError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except SimulationWorkerGatewayError as exc:
raise HTTPException(
status_code=502,
detail="Simulation Worker не подтвердил остановку.",
) from exc
@router.post("/worker/runs/{run_id}/commands")
def command_polygon_worker_run(
run_id: Annotated[
str,
PathParameter(pattern=r"^[a-z0-9][a-z0-9-]{0,63}$"),
],
request: AckermannCommandRequest,
idempotency_key: Annotated[
str,
Header(alias="Idempotency-Key", min_length=1, max_length=160),
],
) -> dict[str, Any]:
gateway, _ = _control_context(
worker_provider,
control_provider,
commit_provider,
)
try:
return gateway.command(
run_id=run_id,
speed_mps=request.speed_mps,
steering_normalized=request.steering_normalized,
idempotency_key=idempotency_key,
)
except SimulationWorkerUnavailableError as exc:
raise HTTPException(status_code=503, detail="Simulation Worker недоступен.") from exc
except SimulationWorkerRejectedError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except SimulationWorkerGatewayError as exc:
raise HTTPException(
status_code=502,
detail="Simulation Worker не подтвердил команду ровера.",
) from exc
return router
def _open_read_only_store(root_provider: RootProvider) -> QualificationRunStore:
root = root_provider()
if root is None:
raise HTTPException(
status_code=503,
detail="Источник журналов Полигона не настроен на этом экземпляре Mission Core.",
)
if not root.is_absolute():
raise HTTPException(
status_code=503,
detail="Источник журналов Полигона настроен некорректно.",
)
try:
return QualificationRunStore(root, read_only=True)
except QualificationRunIntegrityError as exc:
raise HTTPException(
status_code=503,
detail="Источник журналов Полигона недоступен или настроен некорректно.",
) from exc
def _load_run(
root_provider: RootProvider,
run_id: str,
) -> tuple[QualificationRunStore, QualificationRun]:
store = _open_read_only_store(root_provider)
try:
return store, store.load(run_id)
except QualificationRunNotFoundError as exc:
raise HTTPException(status_code=404, detail="Прогон Полигона не найден.") from exc
except QualificationRunIntegrityError as exc:
raise HTTPException(
status_code=500,
detail="Доказательства прогона не прошли проверку целостности.",
) from exc
def _read_registered_json(
store: QualificationRunStore,
run: QualificationRun,
*,
artifact_kind: str | tuple[str, ...],
expected_schema: str | tuple[str, ...],
frame_id: str | None = None,
) -> dict[str, Any]:
artifact_kinds = (artifact_kind,) if isinstance(artifact_kind, str) else artifact_kind
expected_schemas = (
(expected_schema,) if isinstance(expected_schema, str) else expected_schema
)
candidates = [
artifact for artifact in run.artifacts if artifact.kind in artifact_kinds
]
if frame_id is not None:
candidates = [
artifact for artifact in candidates if Path(artifact.relative_path).stem == frame_id
]
if len(candidates) != 1:
raise HTTPException(
status_code=404,
detail="Квалификационный артефакт для этого прогона не найден.",
)
artifact = candidates[0]
if artifact.byte_length > MAX_QUALIFICATION_ARTIFACT_BYTES:
raise HTTPException(status_code=500, detail="Квалификационный артефакт слишком велик.")
run_root = (store.root / run.run_id).resolve()
path = run_root / artifact.relative_path
try:
resolved = path.resolve(strict=True)
resolved.relative_to(run_root)
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=500,
detail="Путь квалификационного артефакта нарушает границу прогона.",
) from exc
if (
path.is_symlink()
or not resolved.is_file()
or resolved.stat().st_size != artifact.byte_length
):
raise HTTPException(
status_code=500,
detail="Квалификационный артефакт не прошёл проверку файла.",
)
digest = hashlib.sha256(resolved.read_bytes()).hexdigest()
if digest != artifact.sha256:
raise HTTPException(
status_code=500,
detail="Квалификационный артефакт не прошёл проверку SHA-256.",
)
try:
value = json.loads(resolved.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=500,
detail="Квалификационный артефакт не является допустимым JSON.",
) from exc
if not isinstance(value, dict) or value.get("schema_version") not in expected_schemas:
raise HTTPException(
status_code=500,
detail="Квалификационный артефакт имеет неизвестную схему.",
)
return value
def _read_ground_review_manifest(
root_provider: RootProvider,
run_id: str,
) -> tuple[Path, dict[str, Any]]:
runs_root = root_provider()
if runs_root is None or not runs_root.is_absolute():
raise HTTPException(
status_code=503,
detail="Источник визуальных прогонов не настроен.",
)
configured = os.environ.get(POLYGON_REVIEW_ROOT_ENV, "").strip()
review_root = (
Path(configured).expanduser() if configured else runs_root.parent / "polygon-review-packs"
)
if not review_root.is_absolute():
raise HTTPException(
status_code=503,
detail="Источник визуальных прогонов настроен некорректно.",
)
run_review_root = review_root / run_id
try:
resolved_review_root = review_root.resolve()
resolved_run_root = run_review_root.resolve(strict=True)
resolved_run_root.relative_to(resolved_review_root)
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=404,
detail="Покадровый просмотр для прогона ещё не опубликован.",
) from exc
candidates = sorted(
path
for path in resolved_run_root.iterdir()
if path.is_dir()
and not path.is_symlink()
and re.fullmatch(r"review-[a-f0-9]{64}", path.name)
and (path / "manifest.json").is_file()
)
if not candidates:
raise HTTPException(
status_code=404,
detail="Покадровый просмотр для прогона ещё не опубликован.",
)
pack_root = candidates[-1]
manifest_path = pack_root / "manifest.json"
try:
if manifest_path.is_symlink() or manifest_path.stat().st_size > MAX_REVIEW_MANIFEST_BYTES:
raise OSError
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=500,
detail="Манифест визуального прогона повреждён.",
) from exc
frames = manifest.get("frames") if isinstance(manifest, dict) else None
safety = manifest.get("safety") if isinstance(manifest, dict) else None
if (
not isinstance(manifest, dict)
or manifest.get("schema_version")
not in {GOOSE_REVIEW_PACK_SCHEMA, RELLIS_REVIEW_PACK_SCHEMA}
or manifest.get("source_run_id") != run_id
or not isinstance(manifest.get("identity_sha256"), str)
or not re.fullmatch(r"[a-f0-9]{64}", manifest["identity_sha256"])
or pack_root.name != f"review-{manifest['identity_sha256']}"
or not isinstance(manifest.get("source_id"), str)
or not isinstance(manifest.get("preview_points"), int)
or not isinstance(frames, list)
or not 1 <= len(frames) <= MAX_REVIEW_FRAMES
or manifest.get("frame_count") != len(frames)
or not isinstance(safety, dict)
or safety.get("visualization_only") is not True
or safety.get("navigation_or_safety_accepted") is not False
):
raise HTTPException(
status_code=500,
detail="Манифест визуального прогона нарушает контракт.",
)
seen: set[str] = set()
for expected_sequence, frame in enumerate(frames):
if (
not isinstance(frame, dict)
or frame.get("sequence") != expected_sequence
or not isinstance(frame.get("frame_id"), str)
or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", frame["frame_id"])
or frame["frame_id"] in seen
or not isinstance(frame.get("source_point_count"), int)
or not isinstance(frame.get("point_count"), int)
or not 1 <= frame["point_count"] <= MAX_REVIEW_POINTS
or frame["source_point_count"] < frame["point_count"]
or not isinstance(frame.get("relative_path"), str)
or not isinstance(frame.get("sha256"), str)
or not re.fullmatch(r"[a-f0-9]{64}", frame["sha256"])
or not isinstance(frame.get("byte_length"), int)
or not 1 <= frame["byte_length"] <= MAX_REVIEW_FRAME_BYTES
or not _valid_review_metrics(frame.get("current"))
or not _valid_review_metrics(frame.get("patchworkpp"))
or not isinstance(frame.get("ground_iou_delta"), (int, float))
or not math.isfinite(frame["ground_iou_delta"])
or not -1 <= frame["ground_iou_delta"] <= 1
):
raise HTTPException(
status_code=500,
detail="Индекс кадров визуального прогона нарушает контракт.",
)
seen.add(frame["frame_id"])
return pack_root, manifest
def _dataset_frame_identity(frame: dict[str, Any]) -> dict[str, str | int | None]:
frame_id = frame["frame_id"]
explicit_sequence = frame.get("dataset_sequence_id")
explicit_number = frame.get("dataset_frame_number")
if explicit_sequence is not None or explicit_number is not None:
if (
not isinstance(explicit_sequence, str)
or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,95}", explicit_sequence)
is None
or not isinstance(explicit_number, int)
or explicit_number < 0
):
raise HTTPException(
status_code=500,
detail="Идентичность dataset-кадра нарушает контракт.",
)
return {
"dataset_sequence_id": explicit_sequence,
"dataset_frame_number": explicit_number,
"sensor_timestamp_ns": None,
}
match = GOOSE_FRAME_ID_PATTERN.fullmatch(frame_id)
if match is None:
raise HTTPException(
status_code=500,
detail="Идентичность GOOSE-кадра нарушает контракт.",
)
return {
"dataset_sequence_id": match.group("dataset_sequence_id"),
"dataset_frame_number": int(match.group("dataset_frame_number")),
# Nanosecond Unix time exceeds JavaScript's safe integer range.
"sensor_timestamp_ns": match.group("sensor_timestamp_ns"),
}
def _valid_review_metrics(value: object) -> bool:
if not isinstance(value, dict):
return False
for key in (
"ground_iou",
"natural_ground_recall",
"obstacle_non_ground_recall",
):
metric = value.get(key)
if (
not isinstance(metric, (int, float))
or not math.isfinite(metric)
or not 0 <= metric <= 1
):
return False
latency = value.get("latency_ms")
return isinstance(latency, (int, float)) and math.isfinite(latency) and latency >= 0
def _verified_review_frame_path(pack_root: Path, frame: dict[str, Any]) -> Path:
relative = Path(frame["relative_path"])
if relative.is_absolute() or relative.parts[:1] != ("frames",):
raise HTTPException(
status_code=500,
detail="Кадр визуального прогона нарушает границу.",
)
path = pack_root / relative
try:
resolved = path.resolve(strict=True)
resolved.relative_to(pack_root)
if (
path.is_symlink()
or not resolved.is_file()
or resolved.stat().st_size != frame["byte_length"]
):
raise OSError
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=500,
detail="Файл визуального прогона нарушает границу.",
) from exc
if hashlib.sha256(resolved.read_bytes()).hexdigest() != frame["sha256"]:
raise HTTPException(
status_code=500,
detail="Кадр визуального прогона не прошёл SHA-256.",
)
return resolved
def _run_summary(run: QualificationRun) -> dict[str, Any]:
return {
"run_id": run.run_id,
"episode_id": run.episode_id,
"kind": run.kind.value,
"state": run.state.value,
"created_at_utc": run.created_at_utc,
"started_at_utc": run.started_at_utc,
"ended_at_utc": run.ended_at_utc,
"terminal_reason": run.terminal_reason,
"scenario_generation": run.scenario_generation,
"profile_generation": run.profile_generation,
"mission_core_commit": run.mission_core_commit,
"host_profile_id": run.host_profile_id,
"reproducibility_tier": run.reproducibility_tier.value,
"clock_domain": run.clock_domain,
"revision": run.revision,
"provider_ids": [provider.identifier for provider in run.providers],
"artifact_count": len(run.artifacts),
}
def _required_worker(provider: WorkerProvider) -> PolygonWorkerGateway:
gateway = provider()
if gateway is None:
raise HTTPException(
status_code=503,
detail="Simulation Worker не зарегистрирован на этом экземпляре Mission Core.",
)
return gateway
def _control_context(
worker_provider: WorkerProvider,
control_provider: ControlProvider,
commit_provider: CommitProvider,
) -> tuple[PolygonWorkerGateway, str]:
if not control_provider():
raise HTTPException(
status_code=403,
detail="Lifecycle Полигона отключён на этом экземпляре Mission Core.",
)
gateway = _required_worker(worker_provider)
commit = commit_provider()
if commit is None:
raise HTTPException(
status_code=503,
detail="Mission Core не привязан к точной Git-ревизии.",
)
return gateway, commit
def _unavailable_worker_status() -> dict[str, Any]:
return {
"schema_version": STATUS_SCHEMA,
"worker_id": "mission-gpu-s1",
"transport": "unix",
"mode": "simulation",
"available": False,
"control_available": False,
"active_run_id": None,
"run_state": None,
"active_provider_ids": [],
"provider_profile": None,
"isolation": {
"network": "unavailable",
"process_identity": "missioncore",
"artifact_policy": "d-only",
},
"authority": {
"scope": "virtual-only",
"actuator_authority": False,
"direct_actuator_setpoints_allowed": False,
},
}
def _new_run_id(commit: str) -> str:
stamp = datetime.now(UTC).strftime("%Y%m%dt%H%M%Sz").lower()
return f"s1c-{commit[:7]}-{stamp}-{secrets.token_hex(3)}"