"""Content-addressed, paired display derivatives, never planning references. Only an offline publisher may attach an explicitly reviewed version to a source overview. HTTP callers select a digest, not filesystem paths or a mutable latest map. Both representations preserve point/pose correspondence and share colors. """ from __future__ import annotations import hashlib import io import json import os import re from dataclasses import dataclass from pathlib import Path from tempfile import TemporaryDirectory from uuid import uuid4 import numpy as np from k1link.reconstruction.map_version import json_bytes SCHEMA = "missioncore.overview-comparison/v1" MAX_POINTS = 180_000 MAX_POSES = 20_000 MAX_BYTES = 8 * 1024 * 1024 def _digest(value): if not isinstance(value, str) or not re.fullmatch(r"[a-f0-9]{64}", value): raise ValueError("Invalid comparison identity.") return value def _read(path, maximum): if path.is_symlink() or path.stat().st_size > maximum: raise ValueError("Invalid comparison artifact.") with path.open("rb") as stream: value = stream.read(maximum + 1) if len(value) > maximum: raise ValueError("Comparison exceeds display budget.") return value @dataclass(frozen=True) class OverviewComparison: generation: str document: dict original: np.ndarray corrected: np.ndarray original_route: np.ndarray corrected_route: np.ndarray @property def bounds(self): # Only bounds are combined; there is no correspondence or fitting here. return np.array( [ self.original.min(axis=0), self.original.max(axis=0), self.corrected.min(axis=0), self.corrected.max(axis=0), ] ) def metadata(self): return dict( generation=self.generation, map_generation=self.document["map_generation"], sample_points=len(self.original), source_points=self.document["source_points"], height_min_m=min(-3.0, float(self.bounds[:, 2].min())), height_max_m=max(80.0, float(self.bounds[:, 2].max())), original_path_m=self.document["original_path_m"], corrected_path_m=self.document["corrected_path_m"], ) def _arrays(payload): # np.savez (uncompressed) is deliberate: reject compressed archive bombs. import zipfile with zipfile.ZipFile(io.BytesIO(payload)) as archive: if len(archive.infolist()) != 4 or any( x.compress_type != zipfile.ZIP_STORED for x in archive.infolist() ): raise ValueError("Invalid comparison array archive.") with np.load(io.BytesIO(payload), allow_pickle=False) as data: result = [ np.array(data[k]) for k in ("original", "corrected", "original_route", "corrected_route") ] a, b, p, q = result if ( a.shape != b.shape or p.shape != q.shape or not 1 <= len(a) <= MAX_POINTS or not 2 <= len(p) <= MAX_POSES or any( x.ndim != 2 or x.shape[1:] != (3,) or x.dtype != np.dtype("